# AlgoPath > Learn algorithms and data structures by building them yourself, in JavaScript, TypeScript, Python, PHP, Java or C#. AlgoPath is a LeetCode alternative for data structures and algorithms interview preparation: instead of grinding a problem list, you implement every algorithm yourself in JavaScript, TypeScript, Python, PHP, Java or C#. It is a 150-step course in active recall. Each step is one arc: the theory, your own implementation, then a real production case where the algorithm is actually needed. On top of the course there are 52 pattern pages, a recognition drill that trains naming the pattern before writing code, a glossary of 388 algorithm and data structure terms and timed interview practice. The first 33 steps are free. One payment opens all 150 steps for 12 months, kept open while the course runs, with no subscription. The pricing page shows the current price and whether a free beta is open. AlgoPath at algopath.pro is the pattern-recognition trainer and 150-step algorithms course made by Elman Huseynov. It is unrelated to other products that use the AlgoPath name. The pattern pages, the pattern list, the glossary, the articles and the site pages are also served as plain markdown: add .md to the address. The free lessons are listed below and open to read and run without an account. The paid ones need a purchase and are deliberately not listed. ## Main pages - [AlgoPath: learn data structures and algorithms by building them yourself](https://algopath.pro) - [Pricing: one payment for 12 months of access, no subscription](https://algopath.pro/pricing) - [Course roadmap: 150 steps from arrays to graphs](https://algopath.pro/roadmap) - [Levels: what each rank means](https://algopath.pro/levels) - [Placement test: which DSA topic to start with](https://algopath.pro/placement) - [Algorithm patterns, and which one a problem needs](https://algopath.pro/patterns) - [LeetCode patterns cheat sheet: every pattern on one page](https://algopath.pro/patterns/cheat-sheet) - [Big O calculator: measure your code's time complexity](https://algopath.pro/big-o-calculator) - [Glossary: 388 algorithm and data structure terms explained](https://algopath.pro/glossary) - [Blog: coding interview preparation and algorithm patterns](https://algopath.pro/blog) - [Reviews from people who used AlgoPath](https://algopath.pro/reviews) - [Contact: ask a question about the course](https://algopath.pro/contact) ## Algorithm patterns --- title: "In-place array transform" url: https://algopath.pro/patterns/in-place-array language: en summary: "A write index trails the read index over one buffer. You copy back only what you keep, so a second array is never allocated." updated: 2026-10-17 --- # In-place array transform A write index trails the read index over one buffer. You copy back only what you keep, so a second array is never allocated. ## How does In-place array transform work? Two indices walk the same array. read visits every slot, write marks where the next kept value goes. read moves on every step of the loop. write moves only when a value survives the test. A kept value is copied to arr[write]. Then write advances by one slot. A dropped value leaves write where it was. The next keeper will overwrite that slot. write can never overtake read, so a copy never destroys unread data. That is why one buffer is enough. At the end write holds the new logical length. The slots past it are leftovers. Sources: [Wikipedia: In-place algorithm](https://en.wikipedia.org/wiki/In-place_algorithm) - `[1, 1, 2, 2, 3] write=1 read=1` Removing duplicates from a sorted array. The first value always survives. - `[1, 1, 2, 2, 3] write=1 read=2` arr[1] repeats arr[0] and is dropped. write does not move. - `[1, 2, 2, 2, 3] write=2 read=3` arr[2] is new, so it lands in slot 1. write advances. - `[1, 2, 2, 2, 3] write=2 read=4` arr[3] repeats the 2 already kept. It is dropped. - `[1, 2, 3, 2, 3] write=3` 3 lands in slot 2. The answer is length 3, and the tail is junk. ## When should you use In-place array transform? Use In-place array transform when the problem statement says “modify the array in place”, “O(1) extra space” or “do not allocate another array/string”. A solution built on it usually runs in O(n). - modify the array in place - O(1) extra space - do not allocate another array/string - reverse/rotate/partition/move elements over a single buffer - return the new length after removing duplicates ## What is In-place array transform confused with? In-place array transform is most often confused with Two pointers (same direction), Hash set / map, Elementary sorts (selection, bubble, insertion) and Prefix sums. - **Two pointers (same direction)** - A slow and fast pair is one mechanic that rewrites a buffer. Working in place is the goal, not the mechanic. - **Hash set / map** - A set drops duplicates in one pass but stores every key. In-place work exists to avoid that memory. - **Elementary sorts (selection, bubble, insertion)** - Insertion sort also rewrites one buffer, but it reorders values. Here the surviving order never changes. - **Prefix sums** - Prefix sums build a second array of running totals. That second array is the thing being avoided. ## What is the time and space complexity of In-place array transform? The target complexity of In-place array transform is O(n). n up to 1e6 with a single pass gives O(n) time. Space stays O(1) because nothing new is allocated. ## A worked example of In-place array transform ### Move zeroes to the end You get an array of numbers and have to move every zero to the end. The order of the non-zero values must survive. Allocating a second array is not allowed. Walk the array with a read index. Copy each non-zero value to arr[write] and advance write. After the pass, write marks where the zeroes begin. Fill everything from there with zero. ```javascript function moveZeroes(nums) { let write = 0; for (let read = 0; read < nums.length; read++) { if (nums[read] !== 0) { nums[write] = nums[read]; write++; } } // everything from write onwards is a leftover slot while (write < nums.length) { nums[write] = 0; write++; } return nums; } ``` ## Common mistakes with In-place array transform - **Advancing write on every step** Moving write on every step keeps it equal to read, so nothing is removed. write moves only on a keep. - **Cutting the tail during the pass** The leftover slots still hold values read has not reached. Trim or overwrite them after the loop. - **Returning the array instead of the length** These tasks usually ask for the new length. The buffer itself keeps its original size. - **Comparing against the wrong slot** A duplicate check must read arr[write - 1], not arr[read - 1]. Earlier slots have already been rewritten. ## Which interview problems use In-place array transform? Interview problems solved with In-place array transform include Remove duplicates from sorted array, Remove element, Move zeroes, Sort colors, Rotate array, Reverse string and Remove duplicates from sorted array II. - **Remove duplicates from sorted array** Keep a value only when it differs from arr[write - 1]. - **Remove element** Keep everything that does not equal the target value. - **Move zeroes** Copy the non-zeroes forward, then pad the tail with zeroes. - **Sort colors** Three indices rewrite one buffer in a single pass. - **Rotate array** Reverse the whole array, then reverse each of the two parts. - **Reverse string** Swap from both ends inward with no new buffer. - **Remove duplicates from sorted array II** Keep a value while it differs from arr[write - 2]. ## JavaScript ```javascript function inPlaceTransform(arr) { let write = 0; for (let read = 0; read < arr.length; read++) { if (shouldKeep(arr[read])) { arr[write] = arr[read]; write++; } } return write; // new logical length } ``` ## TypeScript ```typescript declare function shouldKeep(value: number): boolean; function inPlaceTransform(arr: number[]): number { let write = 0; for (let read = 0; read < arr.length; read++) { if (shouldKeep(arr[read])) { arr[write] = arr[read]; write++; } } return write; // new logical length } ``` ## Python ```python def in_place_transform(arr): write = 0 for read in range(len(arr)): if should_keep(arr[read]): arr[write] = arr[read] write += 1 return write # new logical length ``` ## PHP ```php function inPlaceTransform(array &$arr): int { $write = 0; for ($read = 0; $read < count($arr); $read++) { if (shouldKeep($arr[$read])) { $arr[$write] = $arr[$read]; $write++; } } return $write; // new logical length } ``` ## Java ```java public static int inPlaceTransform(int[] arr) { int write = 0; for (int read = 0; read < arr.length; read++) { if (shouldKeep(arr[read])) { arr[write] = arr[read]; write++; } } return write; // new logical length } private static boolean shouldKeep(int value) { throw new UnsupportedOperationException("your rule here"); } ``` ## C# ```csharp public static int InPlaceTransform(int[] arr) { int write = 0; for (int read = 0; read < arr.Length; read++) { if (ShouldKeep(arr[read])) { arr[write] = arr[read]; write++; } } return write; // new logical length } private static bool ShouldKeep(int value) { throw new NotImplementedException("your rule here"); } ``` --- title: "Hash set / map" url: https://algopath.pro/patterns/hash-set-map language: en summary: "A hash structure turns a lookup into a single step. A set answers whether a value was seen, a map answers how many times." updated: 2026-10-17 --- # Hash set / map A hash structure turns a lookup into a single step. A set answers whether a value was seen, a map answers how many times. ## How does Hash set / map work? A hash function turns a key into a slot number. The value is written at that slot. A lookup runs the same function again. It lands on the slot directly, without scanning anything. Two keys can hash to the same slot. The structure keeps both and compares the keys themselves. A set stores keys and nothing else. Use it for the question have I seen this. A map stores a value beside each key. Use it for counts, indices or the last position seen. Insert, lookup and delete each cost O(1) on average. The price is O(n) memory for the keys. Sources: [Wikipedia: Hash table](https://en.wikipedia.org/wiki/Hash_table), [MDN: Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) - `seen = {}` Looking for the first repeated value in [3, 1, 3, 4]. - `seen = {3}` 3 has not been seen before. It goes into the set. - `seen = {3, 1}` 1 is new as well. The set now holds two keys. - `seen = {3, 1}` 3 comes round again. The set already holds it. - `answer = 3` The answer returns at once. The last element is never read. ## When should you use Hash set / map? Use Hash set / map when the problem statement says “have I seen this before”, “count how many of each” or “find duplicates”. A solution built on it usually runs in O(n). - have I seen this before - count how many of each - find duplicates - does a matching/complement value exist - data is in any order / unsorted ## What is Hash set / map confused with? Hash set / map is most often confused with Two pointers (opposite ends), Sliding window (variable), Non-comparison sort (counting / radix), Trie (prefix tree) and Prefix sums. - **Two pointers (opposite ends)** - Two pointers need a sorted array and use no extra memory. Hashing is the answer when sorting is off the table. - **Sliding window (variable)** - A window answers a question about one contiguous span. A plain map has no idea of a span. - **Non-comparison sort (counting / radix)** - Counting sort also tallies frequencies, but it needs small integer keys. A map takes any key at all. - **Trie (prefix tree)** - A trie shares prefixes between keys, so it can answer prefix queries. A map only matches whole keys. - **Prefix sums** - Prefix sums answer range questions over values kept in order. A hash keeps no order. ## What is the time and space complexity of Hash set / map? The target complexity of Hash set / map is O(n). n up to 1e6 unsorted values gives O(n) time. Space is O(n), because every distinct key is stored. ## A worked example of Hash set / map ### Two sum on unsorted data You get an unsorted array and a target number. Return the indices of two values that add up to it. Sorting would destroy the original indices. So the array has to stay as it is. Walk the array once. For each value, work out the complement the target still needs. If the map already holds that complement, the pair is found. Otherwise store the value with its index. ```javascript function twoSum(nums, target) { const seen = new Map(); // value -> index for (let i = 0; i < nums.length; i++) { const need = target - nums[i]; if (seen.has(need)) { return [seen.get(need), i]; } // stored after the lookup, so a value never pairs with itself seen.set(nums[i], i); } return []; } ``` ## Common mistakes with Hash set / map - **Storing before the lookup** Insert the value only after checking. Otherwise a value can pair with itself. - **Using a plain object for keys** An object silently turns every key into a string, so 1 and "1" collide. Use Map instead. - **Counting without a default** Reading a missing key gives undefined, and undefined plus one is NaN. Start each count at zero. - **Paying for memory you did not need** If the data is already sorted, two pointers cost nothing extra. Hash only when the order is useless. ## Which interview problems use Hash set / map? Interview problems solved with Hash set / map include Two sum, Contains duplicate, Valid anagram, Group anagrams, Top k frequent elements, Longest consecutive sequence and Subarray sum equals k. - **Two sum** Store each value with its index and look up the complement. - **Contains duplicate** A set that rejects a repeat answers this in one pass. - **Valid anagram** Count the letters of one word, then subtract the other. - **Group anagrams** The sorted word is the key. The group is the value. - **Top k frequent elements** Count with a map, then take the k largest counts. - **Longest consecutive sequence** A set lets you ask whether x minus one exists. - **Subarray sum equals k** Store how often each running total has appeared. ## JavaScript ```javascript function firstDuplicate(arr) { const seen = new Set(); for (const x of arr) { if (seen.has(x)) return x; seen.add(x); } return null; } ``` ## TypeScript ```typescript function firstDuplicate(arr: number[]): number | null { const seen = new Set(); for (const x of arr) { if (seen.has(x)) return x; seen.add(x); } return null; } ``` ## Python ```python def first_duplicate(arr): seen = set() for x in arr: if x in seen: return x seen.add(x) return None ``` ## PHP ```php function firstDuplicate(array $arr) { $seen = []; foreach ($arr as $x) { if (isset($seen[$x])) return $x; $seen[$x] = true; } return null; } ``` ## Java ```java public static Integer firstDuplicate(int[] arr) { Set seen = new HashSet<>(); for (int x : arr) { if (seen.contains(x)) return x; seen.add(x); } return null; } ``` ## C# ```csharp public static int? FirstDuplicate(int[] arr) { var seen = new HashSet(); foreach (int x in arr) { if (seen.Contains(x)) return x; seen.Add(x); } return null; } ``` --- title: "Two pointers (opposite ends)" url: https://algopath.pro/patterns/two-pointers-opposite language: en summary: "Two indices start at the ends of a sorted array and walk toward each other. Each step moves whichever side is holding the answer back." updated: 2026-10-17 --- # Two pointers (opposite ends) Two indices start at the ends of a sorted array and walk toward each other. Each step moves whichever side is holding the answer back. ## How does Two pointers (opposite ends) work? One index sits on the first slot, the other on the last. Together they name a candidate answer. Read the value at each end and combine them. That gives a sum, a width, or a pair to compare. Compare the result against the target. The comparison tells you which end is the problem. If the combination is too small, move the left index right. Sorted order guarantees the value can only rise. If it is too large, move the right index left. That is the only move that can lower it. The loop ends when the two indices meet. Every element was read at most once. Sources: [Wikipedia: 3SUM](https://en.wikipedia.org/wiki/3SUM) - `[2, 4, 5, 8, 11] lo=0 hi=4` The target is 12. The pointers start at the two ends. - `2 + 11 = 13` The sum is one over the target. Only the right end can lower it. - `[2, 4, 5, 8, 11] lo=0 hi=3` Now 2 plus 8 is 10, which falls short. Only the left end can raise it. - `[2, 4, 5, 8, 11] lo=1 hi=3` Now 4 plus 8 is 12. The target is hit exactly. - `answer = [1, 3]` Three comparisons over five elements. Nothing was read twice. ## When should you use Two pointers (opposite ends)? Use Two pointers (opposite ends) when the problem statement says “sorted array (or you are allowed to sort)”, “find a pair/triple with a target sum” or “squeeze from both ends (container, area)”. A solution built on it usually runs in O(n). - sorted array (or you are allowed to sort) - find a pair/triple with a target sum - squeeze from both ends (container, area) - palindrome check ## What is Two pointers (opposite ends) confused with? Two pointers (opposite ends) is most often confused with Sliding window (variable), Hash set / map, Two pointers (same direction) and Binary search (array). - **Sliding window (variable)** - A window keeps a live span and both ends move forward. These pointers start apart and close in. - **Hash set / map** - Hashing finds a complement in unsorted data, at the cost of O(n) memory. Two pointers need order but no memory. - **Two pointers (same direction)** - There both indices move forward, one reading and one writing. Here they approach from opposite ends. - **Binary search (array)** - Binary search hunts one fixed target and halves the range. Here the pair being compared changes every step. ## What is the time and space complexity of Two pointers (opposite ends)? The target complexity of Two pointers (opposite ends) is O(n). n up to 1e6 on sorted input gives O(n) time. Each pointer moves at most n steps, and space is O(1). ## A worked example of Two pointers (opposite ends) ### Container with most water Each number is the height of a vertical line on a chart. Pick the two lines that hold the most water. The area is the shorter line multiplied by the distance between them. Start with the widest possible pair, one line at each end. Moving the taller line inward can never help, because the shorter one caps the area. So move the shorter line and keep the best area seen. ```javascript function maxArea(height) { let lo = 0; let hi = height.length - 1; let best = 0; while (lo < hi) { const area = Math.min(height[lo], height[hi]) * (hi - lo); best = Math.max(best, area); // the shorter side caps the area, so it is the only one worth moving if (height[lo] < height[hi]) lo++; else hi--; } return best; } ``` ## Common mistakes with Two pointers (opposite ends) - **Forgetting that the input must be sorted** The whole method rests on order. On unsorted data the comparison points the wrong way. - **Writing lo <= hi** The condition lo <= hi lets one index pair with itself. A pair of distinct elements needs lo < hi. - **Moving both ends in one step** A valid pair can then be skipped without ever being tested. Move exactly one end per step. - **Reporting a triple twice** After a hit, skip the equal values on both sides. Otherwise duplicates come back as separate answers. ## Which interview problems use Two pointers (opposite ends)? Interview problems solved with Two pointers (opposite ends) include Two sum II (sorted input), Three sum, Container with most water, Valid palindrome, Trapping rain water, Sort colors and Squares of a sorted array. - **Two sum II (sorted input)** The plain form: move the end that fixes the sum. - **Three sum** Fix one value, then run two pointers over the rest. - **Container with most water** Move the shorter side, because it caps the area. - **Valid palindrome** Compare the two ends, then step inward. - **Trapping rain water** Move the side with the smaller wall and bank the water. - **Sort colors** The two ends collect the zeroes and the twos. - **Squares of a sorted array** The largest square always sits at one of the ends. ## JavaScript ```javascript function twoPointers(arr, target) { let lo = 0; let hi = arr.length - 1; while (lo < hi) { const sum = arr[lo] + arr[hi]; if (sum === target) return [lo, hi]; if (sum < target) lo++; else hi--; } return null; } ``` ## TypeScript ```typescript function twoPointers(arr: number[], target: number): [number, number] | null { let lo = 0; let hi = arr.length - 1; while (lo < hi) { const sum = arr[lo] + arr[hi]; if (sum === target) return [lo, hi]; if (sum < target) lo++; else hi--; } return null; } ``` ## Python ```python def two_pointers(arr, target): lo, hi = 0, len(arr) - 1 while lo < hi: s = arr[lo] + arr[hi] if s == target: return (lo, hi) if s < target: lo += 1 else: hi -= 1 return None ``` ## PHP ```php function twoPointers(array $arr, int $target): ?array { $lo = 0; $hi = count($arr) - 1; while ($lo < $hi) { $sum = $arr[$lo] + $arr[$hi]; if ($sum === $target) return [$lo, $hi]; if ($sum < $target) $lo++; else $hi--; } return null; } ``` ## Java ```java public static int[] twoPointers(int[] arr, int target) { int lo = 0; int hi = arr.length - 1; while (lo < hi) { int sum = arr[lo] + arr[hi]; if (sum == target) return new int[] { lo, hi }; if (sum < target) lo++; else hi--; } return null; } ``` ## C# ```csharp public static int[] TwoPointers(int[] arr, int target) { int lo = 0; int hi = arr.Length - 1; while (lo < hi) { int sum = arr[lo] + arr[hi]; if (sum == target) return new int[] { lo, hi }; if (sum < target) lo++; else hi--; } return null; } ``` --- title: "Two pointers (same direction)" url: https://algopath.pro/patterns/two-pointers-same-direction language: en summary: "A read pointer scans ahead while a write pointer trails behind it. The write pointer moves only when a value is worth keeping." updated: 2026-10-17 --- # Two pointers (same direction) A read pointer scans ahead while a write pointer trails behind it. The write pointer moves only when a value is worth keeping. ## How does Two pointers (same direction) work? Both indices start at the front of the array. read scans, and write marks the next output slot. read advances once per loop step. It looks at every element exactly one time. A test decides whether the current value is kept. That test is the only part that changes per problem. On a keep, the value is copied into slot write. Then write advances by one. On a drop, write stands still. The slot waits for the next value that survives. write trails read by the number of drops so far. So the prefix up to write is the answer. Sources: [Wikipedia: In-place algorithm](https://en.wikipedia.org/wiki/In-place_algorithm) - `[3, 2, 3, 4, 2] write=0` Every 3 has to go. arr[0] is a 3, so nothing is written. - `[2, 2, 3, 4, 2] write=1` arr[1] is a 2 and survives. It is copied into slot 0. - `[2, 2, 3, 4, 2] write=1` arr[2] is another 3. write stays where it was. - `[2, 4, 3, 4, 2] write=2` arr[3] is a 4 and survives. It lands in slot 1. - `[2, 4, 2, 4, 2] write=3` The last 2 lands in slot 2. The answer is length 3. ## When should you use Two pointers (same direction)? Use Two pointers (same direction) when the problem statement says “remove duplicates in place, keep order”, “move/partition elements while preserving order” or “compact an array and use O(1) extra space”. A solution built on it usually runs in O(n). - remove duplicates in place, keep order - move/partition elements while preserving order - compact an array and use O(1) extra space - shift zeros or a target value to the end ## What is Two pointers (same direction) confused with? Two pointers (same direction) is most often confused with In-place array transform, Two pointers (opposite ends), Sliding window (variable) and Fast & slow pointers. - **In-place array transform** - Working in one buffer is the goal. A trailing write pointer is one way to reach it. - **Two pointers (opposite ends)** - Those pointers start at the ends of sorted data and converge. These both start at index zero. - **Sliding window (variable)** - A window also moves two indices forward, but it keeps a live span. Here the trailing index is a write slot. - **Fast & slow pointers** - There the fast pointer takes two steps to catch a cycle. Here it takes one and reads. ## What is the time and space complexity of Two pointers (same direction)? The target complexity of Two pointers (same direction) is O(n). n up to 1e6 gives O(n) time and O(1) space. The read pointer sees each element once. ## A worked example of Two pointers (same direction) ### Keep each value at most twice A sorted array can repeat the same value many times. Trim it so no value appears more than twice. Work inside the same array and return the new length. The first two values are always fine, because two copies are allowed. After that, keep arr[read] only when it differs from arr[write - 2]. That slot holds the second copy already written. ```javascript function removeDuplicatesII(nums) { let write = 0; for (let read = 0; read < nums.length; read++) { // nums[write - 2] is the copy two slots back in the output if (write < 2 || nums[read] !== nums[write - 2]) { nums[write] = nums[read]; write++; } } return write; // new logical length } ``` ## Common mistakes with Two pointers (same direction) - **Letting write overtake read** A copy would then destroy data read has not seen. write advances only on a keep. - **Testing against the source array** The check belongs on arr[write - 1], the last value actually kept. arr[read - 1] may already be gone. - **Shifting the tail on every removal** Shifting the tail on each removal turns a linear pass into O(n squared) work. One forward copy per keeper is enough. - **Trusting the slots past write** The slots past write still hold stale values from before the pass. Only the first write slots mean anything. ## Which interview problems use Two pointers (same direction)? Interview problems solved with Two pointers (same direction) include Remove duplicates from sorted array, Remove element, Move zeroes, Remove duplicates from sorted array II, String compression, Is subsequence and Merge sorted array. - **Remove duplicates from sorted array** Keep a value only when it differs from arr[write - 1]. - **Remove element** Keep every value that does not equal the target. - **Move zeroes** Copy the non-zeroes forward, then pad the tail. - **Remove duplicates from sorted array II** The check moves back two slots instead of one. - **String compression** Write the character, then write the length of its run. - **Is subsequence** One pointer walks the short string, one walks the long one. - **Merge sorted array** Write from the back so nothing unread is overwritten. ## JavaScript ```javascript function compact(arr) { let slow = 0; for (let fast = 0; fast < arr.length; fast++) { if (shouldKeep(arr[fast], arr[slow])) { arr[slow] = arr[fast]; slow++; } } return slow; // new length } ``` ## TypeScript ```typescript declare function shouldKeep(candidate: number, kept: number): boolean; function compact(arr: number[]): number { let slow = 0; for (let fast = 0; fast < arr.length; fast++) { if (shouldKeep(arr[fast], arr[slow])) { arr[slow] = arr[fast]; slow++; } } return slow; // new length } ``` ## Python ```python def compact(arr): slow = 0 for fast in range(len(arr)): if should_keep(arr[fast], arr[slow]): arr[slow] = arr[fast] slow += 1 return slow # new length ``` ## PHP ```php function compact(array &$arr): int { $slow = 0; for ($fast = 0; $fast < count($arr); $fast++) { if (shouldKeep($arr[$fast], $arr[$slow])) { $arr[$slow] = $arr[$fast]; $slow++; } } return $slow; // new length } ``` ## Java ```java public static int compact(int[] arr) { int slow = 0; for (int fast = 0; fast < arr.length; fast++) { if (shouldKeep(arr[fast], arr[slow])) { arr[slow] = arr[fast]; slow++; } } return slow; // new length } private static boolean shouldKeep(int candidate, int kept) { throw new UnsupportedOperationException("your rule here"); } ``` ## C# ```csharp public static int Compact(int[] arr) { int slow = 0; for (int fast = 0; fast < arr.Length; fast++) { if (ShouldKeep(arr[fast], arr[slow])) { arr[slow] = arr[fast]; slow++; } } return slow; // new length } private static bool ShouldKeep(int candidate, int kept) { throw new NotImplementedException("your rule here"); } ``` --- title: "Sliding window (fixed size)" url: https://algopath.pro/patterns/sliding-window-fixed language: en summary: "A window of exactly k elements rolls one step at a time. Add the value coming in, drop the one going out, reuse the rest." updated: 2026-10-17 --- # Sliding window (fixed size) A window of exactly k elements rolls one step at a time. Add the value coming in, drop the one going out, reuse the rest. ## How does Sliding window (fixed size) work? Build the first window from the first k elements. Compute its answer once, the slow way. Now roll the window one slot to the right. Exactly one element enters and one leaves. Update the running value from those two elements. Add the newcomer and subtract the one that left. Record the answer for this window before rolling on. The best so far lives in one variable. Repeat until the right edge reaches the last element. There are n minus k plus one windows. No window is ever recomputed from scratch. That is what turns O(nk) into O(n). Sources: [Wikipedia: Moving average](https://en.wikipedia.org/wiki/Moving_average) - `[4, 2, 7] sum = 13` Window size is three. The first window is summed directly. - `[2, 7, 1] sum = 10` 1 enters and 4 leaves. So 13 plus 1 minus 4 is 10. - `best = 13` This window is worse than the first. The best does not move. - `[7, 1, 5] sum = 13` 5 enters and 2 leaves. So 10 plus 5 minus 2 is 13. - `answer = 13` The right edge reached the end. Three windows, five reads. ## When should you use Sliding window (fixed size)? Use Sliding window (fixed size) when the problem statement says “contiguous subarray/substring of a given length k”, “every window of size k” or “rolling average or moving sum”. A solution built on it usually runs in O(n). - contiguous subarray/substring of a given length k - every window of size k - rolling average or moving sum - min/max/count/sum over each fixed span ## What is Sliding window (fixed size) confused with? Sliding window (fixed size) is most often confused with Sliding window (variable), Prefix sums, Monotonic deque (sliding window max/min) and Hash set / map. - **Sliding window (variable)** - A variable window decides its own width from a condition. This one is k wide for the whole pass. - **Prefix sums** - Prefix sums are built once and then answer any range. A fixed window answers about one moving span. - **Monotonic deque (sliding window max/min)** - A sum survives a subtraction, so plain arithmetic is enough. A maximum needs the deque. - **Hash set / map** - A map counts over the whole collection and knows no positions. The window asks only about k neighbours. ## What is the time and space complexity of Sliding window (fixed size)? The target complexity of Sliding window (fixed size) is O(n). n up to 1e6 with window size k gives O(n). Each element enters the window once and leaves once. ## A worked example of Sliding window (fixed size) ### Find every anagram of a word You get a string s and a shorter string p. Return every start index where s holds an anagram of p. An anagram uses the same letters with the same counts. Count the letters of p once. Then roll a window of that same length across s. Each roll adds one letter and removes one. Compare the counts and record the index on a match. ```javascript function findAnagrams(s, p) { if (p.length > s.length) return []; const need = new Array(26).fill(0); const have = new Array(26).fill(0); const at = (c) => c.charCodeAt(0) - 97; for (const c of p) need[at(c)]++; const result = []; for (let i = 0; i < s.length; i++) { have[at(s[i])]++; // one letter leaves as soon as the window is longer than p if (i >= p.length) have[at(s[i - p.length])]--; if (i >= p.length - 1 && need.every((n, j) => n === have[j])) { result.push(i - p.length + 1); } } return result; } ``` ## Common mistakes with Sliding window (fixed size) - **Rebuilding the window every step** Summing k elements at each position costs O(nk). Reuse the previous total instead. - **Recording an answer too early** The first k minus one positions hold a partial window. Start recording once it is full. - **Dropping the wrong element** The value leaving sits at index i minus k. An off-by-one here shifts every window. - **Using a rolling sum for a maximum** Subtracting the element that left cannot restore a maximum. That case needs a monotonic deque. ## Which interview problems use Sliding window (fixed size)? Interview problems solved with Sliding window (fixed size) include Maximum sum subarray of size k, Maximum average subarray I, Find all anagrams in a string, Permutation in string, Repeated DNA sequences, Sliding window maximum and K radius subarray averages. - **Maximum sum subarray of size k** The plain form: one addition and one subtraction per step. - **Maximum average subarray I** The same sum, divided by k at the end. - **Find all anagrams in a string** The window carries letter counts instead of a total. - **Permutation in string** The same counts, but it stops at the first match. - **Repeated DNA sequences** Every window of ten characters, tallied in a map. - **Sliding window maximum** A fixed window whose answer needs a monotonic deque. - **K radius subarray averages** A window centred on each index instead of trailing it. ## JavaScript ```javascript function windowStat(arr, k) { let windowSum = 0; for (let i = 0; i < k; i++) windowSum += arr[i]; let best = windowSum; for (let i = k; i < arr.length; i++) { windowSum += arr[i] - arr[i - k]; best = Math.max(best, windowSum); } return best; } ``` ## TypeScript ```typescript function windowStat(arr: number[], k: number): number { let windowSum = 0; for (let i = 0; i < k; i++) windowSum += arr[i]; let best = windowSum; for (let i = k; i < arr.length; i++) { windowSum += arr[i] - arr[i - k]; best = Math.max(best, windowSum); } return best; } ``` ## Python ```python def window_stat(arr, k): window_sum = sum(arr[:k]) best = window_sum for i in range(k, len(arr)): window_sum += arr[i] - arr[i - k] best = max(best, window_sum) return best ``` ## PHP ```php function windowStat(array $arr, int $k): int { $windowSum = array_sum(array_slice($arr, 0, $k)); $best = $windowSum; for ($i = $k; $i < count($arr); $i++) { $windowSum += $arr[$i] - $arr[$i - $k]; $best = max($best, $windowSum); } return $best; } ``` ## Java ```java public static int windowStat(int[] arr, int k) { int windowSum = 0; for (int i = 0; i < k; i++) windowSum += arr[i]; int best = windowSum; for (int i = k; i < arr.length; i++) { windowSum += arr[i] - arr[i - k]; best = Math.max(best, windowSum); } return best; } ``` ## C# ```csharp public static int WindowStat(int[] arr, int k) { int windowSum = 0; for (int i = 0; i < k; i++) windowSum += arr[i]; int best = windowSum; for (int i = k; i < arr.Length; i++) { windowSum += arr[i] - arr[i - k]; best = Math.Max(best, windowSum); } return best; } ``` --- title: "Sliding window (variable)" url: https://algopath.pro/patterns/sliding-window-variable language: en summary: "Grow the window from the right, and shrink it from the left when the condition breaks. Neither end ever moves backwards." updated: 2026-10-15 --- # Sliding window (variable) Grow the window from the right, and shrink it from the left when the condition breaks. Neither end ever moves backwards. ## How does Sliding window (variable) work? The window starts empty, with both ends at index zero. A counter or a map describes what it holds. The right end steps forward and takes in one element. Update the description with that element. Check the condition against the current window. While it holds, record the width or the count. When it breaks, the left end steps forward. Remove that element from the description. Keep shrinking until the condition holds again. Only then does the right end move on. Neither end ever steps back. So every element enters once and leaves once. - `window = a` Longest stretch with no repeated letter. The best is 1. - `window = ab` b enters and is new. The best becomes 2. - `window = abc` c enters and is new. The best becomes 3. - `window = abcb` A second b enters. The window now holds b twice. - `window = cb` The left end drops a and the first b. The best stays 3. ## When should you use Sliding window (variable)? Use Sliding window (variable) when the problem statement says “longest/shortest contiguous subarray or substring”, “satisfying a condition (sum <= K, at most, without breaking a limit)” or “at most K distinct, no repeated element, contains all of X”. A solution built on it usually runs in O(n). - longest/shortest contiguous subarray or substring - satisfying a condition (sum <= K, at most, without breaking a limit) - at most K distinct, no repeated element, contains all of X - contiguous span, not a subsequence ## What is Sliding window (variable) confused with? Sliding window (variable) is most often confused with Sliding window (fixed size), Two pointers (opposite ends), Prefix sums and Hash set / map. - **Sliding window (fixed size)** - A fixed window is k wide from the first step to the last. This one picks its own width. - **Two pointers (opposite ends)** - Those start apart on sorted data and converge to one pair. Here both ends move the same way. - **Prefix sums** - Prefix sums handle negative values and arbitrary ranges. Shrinking assumes removing a value helps. - **Hash set / map** - A map on its own describes the whole collection. The window needs counts for the live span. ## What is the time and space complexity of Sliding window (variable)? The target complexity of Sliding window (variable) is O(n). n up to 1e6 and one best span gives O(n). Each end advances at most n times. ## A worked example of Sliding window (variable) ### Longest stretch with k distinct characters You get a string and a number k. Find the longest stretch that uses at most k different characters. The stretch has to be contiguous, not scattered. Grow the window to the right and count each character in a map. Once the map holds more than k keys, shrink from the left. Measure the width after every shrink. ```javascript function longestKDistinct(s, k) { const count = new Map(); let left = 0; let best = 0; for (let right = 0; right < s.length; right++) { count.set(s[right], (count.get(s[right]) ?? 0) + 1); while (count.size > k) { const out = s[left]; count.set(out, count.get(out) - 1); if (count.get(out) === 0) count.delete(out); // the key must go, not just the count left++; } best = Math.max(best, right - left + 1); } return best; } ``` ## Common mistakes with Sliding window (variable) - **Leaving a zero count in the map** The size still counts a key whose count fell to zero. Delete the key when it empties. - **Shrinking with an if instead of a while** One removal may not be enough to restore the condition. Shrink until it holds again. - **Using it where values can be negative** Dropping a value from the left may raise the sum. Then shrinking proves nothing. - **Measuring the width wrong** The width is right minus left plus one. Dropping the plus one loses a character. ## Which interview problems use Sliding window (variable)? Interview problems solved with Sliding window (variable) include Longest substring without repeating characters, Minimum size subarray sum, Longest substring with at most k distinct characters, Longest repeating character replacement, Minimum window substring, Fruit into baskets and Max consecutive ones III. - **Longest substring without repeating characters** Shrink until the duplicate letter is gone. - **Minimum size subarray sum** Grow until the sum is enough, then shrink while it stays enough. - **Longest substring with at most k distinct characters** The map size is the condition. - **Longest repeating character replacement** Width minus the most common count must stay under k. - **Minimum window substring** Grow until every needed letter is covered, then tighten. - **Fruit into baskets** At most two distinct types, the same shape as k distinct. - **Max consecutive ones III** Shrink once the window holds more than k zeroes. ## JavaScript ```javascript function longestUnder(arr, limit) { let left = 0; let sum = 0; let best = 0; for (let right = 0; right < arr.length; right++) { sum += arr[right]; while (sum > limit) { sum -= arr[left]; left++; } best = Math.max(best, right - left + 1); } return best; } ``` ## TypeScript ```typescript function longestUnder(arr: number[], limit: number): number { let left = 0; let sum = 0; let best = 0; for (let right = 0; right < arr.length; right++) { sum += arr[right]; while (sum > limit) { sum -= arr[left]; left++; } best = Math.max(best, right - left + 1); } return best; } ``` ## Python ```python def longest_under(arr, limit): left = 0 total = 0 best = 0 for right, x in enumerate(arr): total += x while total > limit: total -= arr[left] left += 1 best = max(best, right - left + 1) return best ``` ## PHP ```php function longestUnder(array $arr, int $limit): int { $left = 0; $sum = 0; $best = 0; for ($right = 0; $right < count($arr); $right++) { $sum += $arr[$right]; while ($sum > $limit) { $sum -= $arr[$left]; $left++; } $best = max($best, $right - $left + 1); } return $best; } ``` ## Java ```java public static int longestUnder(int[] arr, int limit) { int left = 0; int sum = 0; int best = 0; for (int right = 0; right < arr.length; right++) { sum += arr[right]; while (sum > limit) { sum -= arr[left]; left++; } best = Math.max(best, right - left + 1); } return best; } ``` ## C# ```csharp public static int LongestUnder(int[] arr, int limit) { int left = 0; int sum = 0; int best = 0; for (int right = 0; right < arr.Length; right++) { sum += arr[right]; while (sum > limit) { sum -= arr[left]; left++; } best = Math.Max(best, right - left + 1); } return best; } ``` --- title: "Prefix sums" url: https://algopath.pro/patterns/prefix-sums language: en summary: "Store a running total of everything that comes before each index. A range sum then costs one subtraction instead of a loop." updated: 2026-10-17 --- # Prefix sums Store a running total of everything that comes before each index. A range sum then costs one subtraction instead of a loop. ## How does Prefix sums work? Make an array one slot longer than the input. Slot zero is the sum of nothing, so it is zero. Walk the input from left to right. Each slot holds the previous total plus the current value. Now prefix[i] means the sum of the first i values. Nothing from index i onward is included. A range from l to r is the whole prefix up to r. Subtract the part that sits before l. So the answer is prefix[r + 1] minus prefix[l]. That is one subtraction, whatever the width. The array is built once and read many times. It stops being valid if a value changes. Sources: [Wikipedia: Prefix sum](https://en.wikipedia.org/wiki/Prefix_sum) - `prefix = [0]` The input is [3, 1, 4, 1]. Slot zero is the sum of nothing. - `prefix = [0, 3]` Add the first value. One element sums to 3. - `prefix = [0, 3, 4]` Add 1 to the running total. Two elements sum to 4. - `prefix = [0, 3, 4, 8, 9]` The rest finishes the array. Building it took one pass. - `sum(1..2) = 8 - 3 = 5` That range holds 1 and 4. One subtraction, no loop. ## When should you use Prefix sums? Use Prefix sums when the problem statement says “many sum-over-a-range queries”, “answer several range-sum questions on the same array” or “sum from index l to r”. A solution built on it usually runs in O(n) build / O(1) query. - many sum-over-a-range queries - answer several range-sum questions on the same array - sum from index l to r - running total or cumulative sum ## What is Prefix sums confused with? Prefix sums is most often confused with Sliding window (fixed size), Difference array, Fenwick tree / segment tree and Hash set / map. - **Sliding window (fixed size)** - A window answers about one span that keeps moving. Prefix sums answer about any span you name. - **Difference array** - That is the mirror image: many range writes, one read at the end. Here it is many reads. - **Fenwick tree / segment tree** - A prefix array breaks the moment a value changes. A Fenwick tree pays log n to allow updates. - **Hash set / map** - A map has no notion of a range at all. Counting subarrays needs the running total beside it. ## What is the time and space complexity of Prefix sums? The target complexity of Prefix sums is O(n) build / O(1) query. n up to 1e6 with many range queries gives O(n) setup. Each query then costs O(1), plus O(n) memory. ## A worked example of Prefix sums ### Count the subarrays that sum to k You get an array of integers and a number k. Count the contiguous stretches that sum to exactly k. Values can be negative, so a sliding window cannot shrink safely. Walk the array and keep a running total. A stretch ending here sums to k when some earlier prefix equals total minus k. A map of seen prefixes counts them. ```javascript function subarraySum(nums, k) { const seen = new Map([[0, 1]]); // one empty prefix, total zero let total = 0; let count = 0; for (const x of nums) { total += x; // an earlier prefix of total - k closes a stretch that sums to k count += seen.get(total - k) ?? 0; seen.set(total, (seen.get(total) ?? 0) + 1); } return count; } ``` ## Common mistakes with Prefix sums - **Skipping the leading zero** A range starting at index zero then has nothing to subtract. The array needs n plus one slots. - **Off by one at the right edge** The sum from l to r reads prefix[r + 1]. Reading prefix[r] drops the last value. - **Changing a value afterwards** One edit invalidates every slot after it. Use a Fenwick tree when updates are mixed in. - **Letting the total overflow** A million values near 1e9 pass the safe integer range. Switch to BigInt or a wider type. ## Which interview problems use Prefix sums? Interview problems solved with Prefix sums include Range sum query immutable, Subarray sum equals k, Find pivot index, Product of array except self, Continuous subarray sum, Maximum size subarray sum equals k and Range sum query 2D. - **Range sum query immutable** Build once, then answer each query with one subtraction. - **Subarray sum equals k** Pair the running total with a map of earlier prefixes. - **Find pivot index** The left sum must equal the total minus the left sum minus the value. - **Product of array except self** The same idea with prefix and suffix products. - **Continuous subarray sum** Store the prefix totals modulo k instead of the raw totals. - **Maximum size subarray sum equals k** Keep the first index where each prefix appeared. - **Range sum query 2D** Two dimensions, four lookups per query. ## JavaScript ```javascript function buildPrefix(arr) { const prefix = new Array(arr.length + 1).fill(0); for (let i = 0; i < arr.length; i++) { prefix[i + 1] = prefix[i] + arr[i]; } return prefix; } function rangeSum(prefix, l, r) { return prefix[r + 1] - prefix[l]; // sum of arr[l..r] } ``` ## TypeScript ```typescript function buildPrefix(arr: number[]): number[] { const prefix: number[] = new Array(arr.length + 1).fill(0); for (let i = 0; i < arr.length; i++) { prefix[i + 1] = prefix[i] + arr[i]; } return prefix; } function rangeSum(prefix: number[], l: number, r: number): number { return prefix[r + 1] - prefix[l]; // sum of arr[l..r] } ``` ## Python ```python def build_prefix(arr): prefix = [0] * (len(arr) + 1) for i, val in enumerate(arr): prefix[i + 1] = prefix[i] + val return prefix def range_sum(prefix, l, r): return prefix[r + 1] - prefix[l] # sum of arr[l..r] ``` ## PHP ```php function buildPrefix(array $arr): array { $prefix = array_fill(0, count($arr) + 1, 0); foreach ($arr as $i => $val) { $prefix[$i + 1] = $prefix[$i] + $val; } return $prefix; } function rangeSum(array $prefix, int $l, int $r): int { return $prefix[$r + 1] - $prefix[$l]; // sum of arr[l..r] } ``` ## Java ```java public static int[] buildPrefix(int[] arr) { int[] prefix = new int[arr.length + 1]; for (int i = 0; i < arr.length; i++) { prefix[i + 1] = prefix[i] + arr[i]; } return prefix; } public static int rangeSum(int[] prefix, int l, int r) { return prefix[r + 1] - prefix[l]; // sum of arr[l..r] } ``` ## C# ```csharp public static int[] BuildPrefix(int[] arr) { var prefix = new int[arr.Length + 1]; for (int i = 0; i < arr.Length; i++) { prefix[i + 1] = prefix[i] + arr[i]; } return prefix; } public static int RangeSum(int[] prefix, int l, int r) { return prefix[r + 1] - prefix[l]; // sum of arr[l..r] } ``` --- title: "Difference array" url: https://algopath.pro/patterns/difference-array language: en summary: "Record the change at each index instead of the value. A range update costs two writes, and one final pass rebuilds everything." updated: 2026-10-17 --- # Difference array Record the change at each index instead of the value. A range update costs two writes, and one final pass rebuilds everything. ## How does Difference array work? Make an array of zeroes, one slot longer than the input. Each slot means the change at that index. To add v across the range l to r, write two numbers. Add v at l and subtract v at r plus one. Those two writes are the whole update. Nothing between l and r is touched. Repeat for every range you are given. Each one costs two writes, whatever its width. When the updates are done, take a running total across the array. Slot i then holds the real value. The rebuild is a single pass. So m updates over n slots cost O(n + m). Sources: [Wikipedia: Prefix sum](https://en.wikipedia.org/wiki/Prefix_sum) - `diff = [0, 0, 0, 0, 0, 0]` Five real slots plus one guard. Everything starts at zero. - `diff = [0, 2, 0, 0, -2, 0]` Add 2 across the range 1 to 3. Two writes, not three. - `diff = [3, 2, -3, 0, -2, 0]` Add 3 across the range 0 to 1. Again two writes. - `running = [3, 5, 2, 2, 0]` One pass of running totals rebuilds the values. - `answer = [3, 5, 2, 2, 0]` Two updates cost four writes. The width never mattered. ## When should you use Difference array? Use Difference array when the problem statement says “many range updates first, then read the final array once”, “add a value to every element in a range, repeated many times” or “apply k range increments before answering any query”. A solution built on it usually runs in O(n). - many range updates first, then read the final array once - add a value to every element in a range, repeated many times - apply k range increments before answering any query - booking/interval counters over an array ## What is Difference array confused with? Difference array is most often confused with Prefix sums, Sweep line (event counting), Fenwick tree / segment tree and Sliding window (fixed size). - **Prefix sums** - Prefix sums read many ranges from data that never changes. This is the mirror: many writes, one read. - **Sweep line (event counting)** - A sweep sorts events by coordinate, because the coordinates are huge or sparse. Here they index an array directly. - **Fenwick tree / segment tree** - A Fenwick tree answers queries in between updates. A difference array only answers after the last one. - **Sliding window (fixed size)** - A window walks one span of fixed width. The ranges here differ in width and overlap freely. ## What is the time and space complexity of Difference array? The target complexity of Difference array is O(n). n up to 1e6 with m range updates gives O(n + m). Each update is two writes, and the rebuild is one pass. ## A worked example of Difference array ### Seats booked on each flight You run n flights and receive a list of bookings. Each booking adds seats to every flight in a range. Return the total number of seats booked on each flight. A loop per booking would cost O(n) for a wide range. Record each booking as two numbers instead. Take running totals once, after every booking is in. ```javascript function corpFlightBookings(bookings, n) { const diff = new Array(n + 1).fill(0); for (const [first, last, seats] of bookings) { diff[first - 1] += seats; diff[last] -= seats; // one slot past the end of the range } const answer = new Array(n); let running = 0; for (let i = 0; i < n; i++) { running += diff[i]; answer[i] = running; } return answer; } ``` ## Common mistakes with Difference array - **Sizing the array by n exactly** A range ending at the last index writes to slot n. Allocate n plus one slots. - **Subtracting at r instead of r plus one** The right edge is inclusive, so it must keep the change. Subtract one slot later. - **Reading a value before the rebuild** The slots hold changes, not values, until the final pass. Ask questions only after it. - **Writing a single value directly** One index is a range of width one. It still needs both writes. ## Which interview problems use Difference array? Interview problems solved with Difference array include Corporate flight bookings, Range addition, Car pooling, Shifting letters II, Maximum population year, Meeting rooms II and Number of flowers in full bloom. - **Corporate flight bookings** Each booking is two writes over a range of flights. - **Range addition** The plain form of the pattern, with nothing else attached. - **Car pooling** Passengers get on and off, and the total must stay under the limit. - **Shifting letters II** Each shift covers a range of positions in the string. - **Maximum population year** A birth adds one and a death subtracts one. - **Meeting rooms II** A start adds a room, an end gives one back. - **Number of flowers in full bloom** Each flower covers a range of days. ## JavaScript ```javascript function applyRangeUpdates(n, updates) { const diff = new Array(n + 1).fill(0); for (const [l, r, val] of updates) { diff[l] += val; diff[r + 1] -= val; } const result = new Array(n); let running = 0; for (let i = 0; i < n; i++) { running += diff[i]; result[i] = running; } return result; } ``` ## TypeScript ```typescript function applyRangeUpdates(n: number, updates: [number, number, number][]): number[] { const diff: number[] = new Array(n + 1).fill(0); for (const [l, r, val] of updates) { diff[l] += val; diff[r + 1] -= val; } const result: number[] = new Array(n); let running = 0; for (let i = 0; i < n; i++) { running += diff[i]; result[i] = running; } return result; } ``` ## Python ```python def apply_range_updates(n, updates): diff = [0] * (n + 1) for l, r, val in updates: diff[l] += val diff[r + 1] -= val result = [0] * n running = 0 for i in range(n): running += diff[i] result[i] = running return result ``` ## PHP ```php function applyRangeUpdates(int $n, array $updates): array { $diff = array_fill(0, $n + 1, 0); foreach ($updates as [$l, $r, $val]) { $diff[$l] += $val; $diff[$r + 1] -= $val; } $result = array_fill(0, $n, 0); $running = 0; for ($i = 0; $i < $n; $i++) { $running += $diff[$i]; $result[$i] = $running; } return $result; } ``` ## Java ```java public static int[] applyRangeUpdates(int n, int[][] updates) { int[] diff = new int[n + 1]; for (int[] u : updates) { diff[u[0]] += u[2]; diff[u[1] + 1] -= u[2]; } int[] result = new int[n]; int running = 0; for (int i = 0; i < n; i++) { running += diff[i]; result[i] = running; } return result; } ``` ## C# ```csharp public static int[] ApplyRangeUpdates(int n, int[][] updates) { var diff = new int[n + 1]; foreach (var u in updates) { diff[u[0]] += u[2]; diff[u[1] + 1] -= u[2]; } var result = new int[n]; int running = 0; for (int i = 0; i < n; i++) { running += diff[i]; result[i] = running; } return result; } ``` --- title: "Linear search" url: https://algopath.pro/patterns/linear-search language: en summary: "Walk the collection from one end and stop at the first item that matches. No order, no structure and no setup are needed." updated: 2026-10-17 --- # Linear search Walk the collection from one end and stop at the first item that matches. No order, no structure and no setup are needed. ## How does Linear search work? Start at the first element of the collection. Ask whether it is the one you want. If it matches, stop and return its index. There is no reason to look further. If it does not, move to the next element. Ask the same question again. When the end arrives with no match, report failure. Minus one or null is the usual answer. Nothing at all is assumed about the data. It may be sorted, shuffled or still arriving. The worst case reads every element once. The best case reads exactly one. Sources: [Wikipedia: Linear search](https://en.wikipedia.org/wiki/Linear_search) - `i = 0, value 5` Looking for 2 in [5, 8, 2, 9]. The first element misses. - `i = 1, value 8` Still no match. Move on to the next index. - `i = 2, value 2` This element equals the target. - `return 2` The index goes back at once. The 9 is never read. - `target 7 gives -1` A missing value costs a whole pass. That is the worst case. ## When should you use Linear search? Use Linear search when the problem statement says “unsorted array”, “check every element” or “no order guarantee”. A solution built on it usually runs in O(n). - unsorted array - check every element - no order guarantee - find first/any match - small n or one-time scan ## What is Linear search confused with? Linear search is most often confused with Binary search (array), Hash set / map, Two pointers (same direction), Sliding window (variable) and Elementary sorts (selection, bubble, insertion). - **Binary search (array)** - Binary search needs sorted data and pays log n. A scan reads anything, in any order. - **Hash set / map** - A map answers instantly but has to be built first. For a single lookup the scan is cheaper. - **Two pointers (same direction)** - That pair rewrites the array while it scans. A search only ever reads. - **Sliding window (variable)** - A window carries a description of a live span. A scan carries at most the best seen so far. - **Elementary sorts (selection, bubble, insertion)** - Sorting first costs O(n log n) and pays off across many searches. One lookup does not repay it. ## What is the time and space complexity of Linear search? The target complexity of Linear search is O(n). n up to roughly 1e7 gives O(n) time and O(1) space. Bigger, or repeated, wants a real structure. ## A worked example of Linear search ### First character that never repeats You get a string. Return the index of the first character that appears exactly once. If every character repeats, return minus one. Count every character in one pass. A map keyed by the character is enough. Then scan the string again from the left. Return the first index whose count is one. ```javascript function firstUniqChar(s) { const count = new Map(); for (const c of s) { count.set(c, (count.get(c) ?? 0) + 1); } // the second pass is a plain scan: the first hit wins for (let i = 0; i < s.length; i++) { if (count.get(s[i]) === 1) return i; } return -1; } ``` ## Common mistakes with Linear search - **Scanning inside another loop** A scan per element turns O(n) into O(n squared). Build a map once instead. - **Returning the value instead of the index** Most of these tasks want the position. A value cannot say where it was found. - **Forgetting the not-found case** A loop that ends with no match still has to return something. Pick minus one and stay with it. - **Scanning data that was already sorted** Sorted input makes binary search far cheaper. Check the input before writing the loop. ## Which interview problems use Linear search? Interview problems solved with Linear search include Linear search, First unique character in a string, Find the maximum, Contains duplicate on tiny input, Missing number, Find all indices of a value and Majority element. - **Linear search** The plain form: stop at the first match. - **First unique character in a string** Count once, then scan for a count of one. - **Find the maximum** Keep the best seen and compare each element against it. - **Contains duplicate on tiny input** A nested scan is fine when n is very small. - **Missing number** Scan and compare each index against the value expected there. - **Find all indices of a value** The same scan, but it does not stop at the first hit. - **Majority element** One pass with a counter gives the Boyer-Moore vote. ## JavaScript ```javascript function linearSearch(arr, target) { for (let i = 0; i < arr.length; i++) { if (arr[i] === target) return i; } return -1; } ``` ## TypeScript ```typescript function linearSearch(arr: number[], target: number): number { for (let i = 0; i < arr.length; i++) { if (arr[i] === target) return i; } return -1; } ``` ## Python ```python def linear_search(arr, target): for i, x in enumerate(arr): if x == target: return i return -1 ``` ## PHP ```php function linearSearch(array $arr, $target) { foreach ($arr as $i => $x) { if ($x === $target) return $i; } return -1; } ``` ## Java ```java public static int linearSearch(int[] arr, int target) { for (int i = 0; i < arr.length; i++) { if (arr[i] == target) return i; } return -1; } ``` ## C# ```csharp public static int LinearSearch(int[] arr, int target) { for (int i = 0; i < arr.Length; i++) { if (arr[i] == target) return i; } return -1; } ``` --- title: "Binary search (array)" url: https://algopath.pro/patterns/binary-search-array language: en summary: "Look at the middle of a sorted range and throw away the half that cannot hold the answer. Repeat until one item is left." updated: 2026-10-17 --- # Binary search (array) Look at the middle of a sorted range and throw away the half that cannot hold the answer. Repeat until one item is left. ## How does Binary search (array) work? Keep two bounds, low and high, over the sorted range. The answer, if any, lies between them. Take the middle index of that range. Compare the value there against the target. On a match the search is over. Return that index. If the middle value is too small, the target lies to the right. Move low past the middle. If it is too large, move high below the middle. Half the range disappears either way. The loop ends when low passes high. The range is empty, so the target is absent. Sources: [Wikipedia: Binary search](https://en.wikipedia.org/wiki/Binary_search), [Python docs: bisect](https://docs.python.org/3/library/bisect.html) - `lo=0 hi=5` Looking for 7 in [1, 3, 5, 7, 9, 11]. Six candidates. - `mid=2, value 5` 5 is smaller than 7. Everything left of it is out. - `lo=3 hi=5, mid=4, value 9` 9 is larger than 7. Everything right of it is out. - `lo=3 hi=3, mid=3, value 7` One candidate is left, and it matches. - `return 3` Three comparisons for six elements. Doubling the input adds one. ## When should you use Binary search (array)? Use Binary search (array) when the problem statement says “sorted array”, “find a target / first or last occurrence” or “n up to 1e6+ and O(log n) expected”. A solution built on it usually runs in O(log n). - sorted array - find a target / first or last occurrence - n up to 1e6+ and O(log n) expected - find the boundary where a condition flips - search a rotated-but-mostly-sorted array (with a twist) ## What is Binary search (array) confused with? Binary search (array) is most often confused with Linear search, Binary search on a rotated array, Binary search on the answer, Binary search tree and Two pointers (opposite ends). - **Linear search** - A scan reads every element but assumes no order. Binary search demands order and reads log n. - **Binary search on a rotated array** - A rotation breaks the single sorted run. The check must find the sorted half first. - **Binary search on the answer** - There the range is the set of possible answers, not an array. The test is a function you write. - **Binary search tree** - A tree halves the same way but through pointers. It also allows inserts, which an array does not. - **Two pointers (opposite ends)** - Both ends move there, and the pair being tested changes. Here only one bound moves per step. ## What is the time and space complexity of Binary search (array)? The target complexity of Binary search (array) is O(log n). n up to 1e9 is fine, because log2 of 1e9 is about 30. Time is O(log n), space is O(1). ## A worked example of Binary search (array) ### First and last position of a value A sorted array can hold the same value many times over. Return the first and last index of a target. If the value is missing, return minus one twice. Run the search twice with a different tie-break. For the first index, keep pushing high left after a match. For the last, keep pushing low right. ```javascript function searchRange(nums, target) { function bound(findFirst) { let lo = 0; let hi = nums.length - 1; let found = -1; while (lo <= hi) { const mid = lo + Math.floor((hi - lo) / 2); // cannot overflow if (nums[mid] === target) { found = mid; // a match is not the end: keep squeezing the chosen side if (findFirst) hi = mid - 1; else lo = mid + 1; } else if (nums[mid] < target) { lo = mid + 1; } else { hi = mid - 1; } } return found; } return [bound(true), bound(false)]; } ``` ## Common mistakes with Binary search (array) - **Computing mid as lo plus hi over two** That sum can pass the safe integer range. Write lo plus half of the gap. - **Moving a bound to mid instead of past it** The range stops shrinking and the loop never ends. Use mid plus one or mid minus one. - **Mixing the two loop shapes** With lo <= hi both ends are inclusive. Changing one end alone breaks the invariant. - **Searching data that is not sorted** The comparison assumes everything left of mid is smaller. Without that the answer is silently wrong. ## Which interview problems use Binary search (array)? Interview problems solved with Binary search (array) include Binary search, Search insert position, First and last position of an element, Find peak element, Sqrt of x, Search a 2D matrix and Smallest letter greater than target. - **Binary search** The plain form over distinct sorted values. - **Search insert position** Return where the value would belong if it is missing. - **First and last position of an element** Two searches with opposite tie-breaks. - **Find peak element** Compare against the neighbour instead of a target. - **Sqrt of x** The sorted range is the set of answers, not an array. - **Search a 2D matrix** Treat the whole grid as one sorted list. - **Smallest letter greater than target** An upper bound that wraps back to the start. ## JavaScript ```javascript function binarySearch(arr, target) { let lo = 0, hi = arr.length - 1; while (lo <= hi) { const mid = (lo + hi) >> 1; if (arr[mid] === target) return mid; if (arr[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; } ``` ## TypeScript ```typescript function binarySearch(arr: number[], target: number): number { let lo = 0, hi = arr.length - 1; while (lo <= hi) { const mid = (lo + hi) >> 1; if (arr[mid] === target) return mid; if (arr[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; } ``` ## Python ```python def binary_search(arr, target): lo, hi = 0, len(arr) - 1 while lo <= hi: mid = (lo + hi) // 2 if arr[mid] == target: return mid if arr[mid] < target: lo = mid + 1 else: hi = mid - 1 return -1 ``` ## PHP ```php function binarySearch(array $arr, $target) { $lo = 0; $hi = count($arr) - 1; while ($lo <= $hi) { $mid = intdiv($lo + $hi, 2); if ($arr[$mid] === $target) return $mid; if ($arr[$mid] < $target) $lo = $mid + 1; else $hi = $mid - 1; } return -1; } ``` ## Java ```java public static int binarySearch(int[] arr, int target) { int lo = 0, hi = arr.length - 1; while (lo <= hi) { int mid = (lo + hi) >>> 1; if (arr[mid] == target) return mid; if (arr[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; } ``` ## C# ```csharp public static int BinarySearch(int[] arr, int target) { int lo = 0, hi = arr.Length - 1; while (lo <= hi) { int mid = (int)(((uint)(lo + hi)) >> 1); if (arr[mid] == target) return mid; if (arr[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; } ``` --- title: "Binary search on a rotated array" url: https://algopath.pro/patterns/binary-search-rotated language: en summary: "A sorted array that was cut and swapped still has one sorted half at every step. Find that half, then pick a side to keep." updated: 2026-10-17 --- # Binary search on a rotated array A sorted array that was cut and swapped still has one sorted half at every step. Find that half, then pick a side to keep. ## How does Binary search on a rotated array work? A rotated array is one sorted run, cut and swapped. Each of the two pieces is still sorted. Take the middle index as usual. Compare the value there with the value at the low bound. If the low value is not larger, the left half is the sorted one. Otherwise the right half is. Now one half is fully understood. Its two ends bracket every value inside it. Check whether the target falls between those ends. If it does, search that half, otherwise search the other. Either way half of the range disappears. The cost stays log n. Sources: [Wikipedia: Binary search](https://en.wikipedia.org/wiki/Binary_search) - `lo=0 hi=6, mid=3, value 7` Searching [4, 5, 6, 7, 0, 1, 2] for a 0. - `left half 4 to 7 is sorted` The low value is 4 and mid is 7. So the left side never wraps. - `lo=4 hi=6` 0 does not fall between 4 and 7. The answer must be on the right. - `mid=5, value 1, left half 0 to 1` The left half is sorted again. This time 0 falls inside it. - `lo=4 hi=4, value 0` One candidate is left and it matches. Three steps for seven elements. ## When should you use Binary search on a rotated array? Use Binary search on a rotated array when the problem statement says “sorted array rotated at an unknown pivot”, “find a target in O(log n)” or “no duplicates (or handle them separately)”. A solution built on it usually runs in O(log n). - sorted array rotated at an unknown pivot - find a target in O(log n) - no duplicates (or handle them separately) - find the minimum / the rotation point - array increases then drops once ## What is Binary search on a rotated array confused with? Binary search on a rotated array is most often confused with Binary search (array), Linear search, Binary search on the answer and Fast sort (merge / quick). - **Binary search (array)** - A plain search compares the middle against the target. Here it is compared against a bound first. - **Linear search** - A scan handles a rotated array with no reasoning at all. It costs O(n) instead of O(log n). - **Binary search on the answer** - There the range is made of candidate answers. Here you are searching real stored values. - **Fast sort (merge / quick)** - Sorting restores the order but costs n log n. The rotation already leaves enough order to use. ## What is the time and space complexity of Binary search on a rotated array? The target complexity of Binary search on a rotated array is O(log n). n up to 1e9 gives O(log n), the same as a plain search. Duplicates push the worst case to O(n). ## A worked example of Binary search on a rotated array ### Smallest value in a rotated array A sorted array was rotated an unknown number of times. Find its smallest value. The values are all distinct, and scanning the whole array is not allowed. Compare the middle value against the value at the high bound. If mid is larger, the rotation point lies to its right. Otherwise the minimum is mid or left of it. ```javascript function findMin(nums) { let lo = 0; let hi = nums.length - 1; while (lo < hi) { const mid = lo + Math.floor((hi - lo) / 2); // compare against the right end, never the left if (nums[mid] > nums[hi]) { lo = mid + 1; // the rotation point is further right } else { hi = mid; // mid may itself be the minimum } } return nums[lo]; } ``` ## Common mistakes with Binary search on a rotated array - **Comparing the middle with the target first** The target says nothing about which half is sorted. Compare the middle against a bound instead. - **Moving high past mid while hunting a minimum** The middle element may be the minimum itself. Only the low bound may skip over it. - **Assuming a rotation actually happened** A rotation of zero is still valid input. The sorted-half test has to cover that case. - **Ignoring duplicate values** When both bounds hold the same value, neither half can be identified. The only safe move shrinks by one. ## Which interview problems use Binary search on a rotated array? Interview problems solved with Binary search on a rotated array include Search in rotated sorted array, Search in rotated sorted array II, Find minimum in rotated sorted array, Find minimum in rotated sorted array II, Find how far the array was rotated, Peak index in a mountain array and Find in mountain array. - **Search in rotated sorted array** The plain form, with distinct values. - **Search in rotated sorted array II** Duplicates force a linear worst case. - **Find minimum in rotated sorted array** Compare against the high bound instead of the target. - **Find minimum in rotated sorted array II** The same, with the duplicate case handled by hand. - **Find how far the array was rotated** The index of the minimum is the rotation count. - **Peak index in a mountain array** The same halving, driven by a neighbour comparison. - **Find in mountain array** Locate the peak first, then search each side. ## JavaScript ```javascript function searchRotated(arr, target) { let lo = 0, hi = arr.length - 1; while (lo <= hi) { const mid = (lo + hi) >> 1; if (arr[mid] === target) return mid; if (arr[lo] <= arr[mid]) { if (arr[lo] <= target && target < arr[mid]) hi = mid - 1; else lo = mid + 1; } else { if (arr[mid] < target && target <= arr[hi]) lo = mid + 1; else hi = mid - 1; } } return -1; } ``` ## TypeScript ```typescript function searchRotated(arr: number[], target: number): number { let lo = 0, hi = arr.length - 1; while (lo <= hi) { const mid = (lo + hi) >> 1; if (arr[mid] === target) return mid; if (arr[lo] <= arr[mid]) { if (arr[lo] <= target && target < arr[mid]) hi = mid - 1; else lo = mid + 1; } else { if (arr[mid] < target && target <= arr[hi]) lo = mid + 1; else hi = mid - 1; } } return -1; } ``` ## Python ```python def search_rotated(arr, target): lo, hi = 0, len(arr) - 1 while lo <= hi: mid = (lo + hi) // 2 if arr[mid] == target: return mid if arr[lo] <= arr[mid]: if arr[lo] <= target < arr[mid]: hi = mid - 1 else: lo = mid + 1 else: if arr[mid] < target <= arr[hi]: lo = mid + 1 else: hi = mid - 1 return -1 ``` ## PHP ```php function searchRotated(array $arr, $target) { $lo = 0; $hi = count($arr) - 1; while ($lo <= $hi) { $mid = intdiv($lo + $hi, 2); if ($arr[$mid] === $target) return $mid; if ($arr[$lo] <= $arr[$mid]) { if ($arr[$lo] <= $target && $target < $arr[$mid]) $hi = $mid - 1; else $lo = $mid + 1; } else { if ($arr[$mid] < $target && $target <= $arr[$hi]) $lo = $mid + 1; else $hi = $mid - 1; } } return -1; } ``` ## Java ```java public static int searchRotated(int[] arr, int target) { int lo = 0, hi = arr.length - 1; while (lo <= hi) { int mid = (lo + hi) >>> 1; if (arr[mid] == target) return mid; if (arr[lo] <= arr[mid]) { if (arr[lo] <= target && target < arr[mid]) hi = mid - 1; else lo = mid + 1; } else { if (arr[mid] < target && target <= arr[hi]) lo = mid + 1; else hi = mid - 1; } } return -1; } ``` ## C# ```csharp public static int SearchRotated(int[] arr, int target) { int lo = 0, hi = arr.Length - 1; while (lo <= hi) { int mid = (int)(((uint)(lo + hi)) >> 1); if (arr[mid] == target) return mid; if (arr[lo] <= arr[mid]) { if (arr[lo] <= target && target < arr[mid]) hi = mid - 1; else lo = mid + 1; } else { if (arr[mid] < target && target <= arr[hi]) lo = mid + 1; else hi = mid - 1; } } return -1; } ``` --- title: "Binary search on the answer" url: https://algopath.pro/patterns/binary-search-on-answer language: en summary: "The thing you search here is the range of every possible answer. A yes-or-no test decides which half of it to throw away." updated: 2026-10-22 --- # Binary search on the answer The thing you search here is the range of every possible answer. A yes-or-no test decides which half of it to throw away. ## How does Binary search on the answer work? Name the smallest and largest value the answer could take. That pair is the search range. Write a test that asks whether one candidate works. It answers only yes or no. The test must be monotone: once it says yes, every larger candidate says yes. Without that, halving is invalid. Take the middle candidate and run the test. Note that this is a value, not an index. On a yes, remember it and search the smaller half. On a no, search the larger half. To maximise a minimum, the test flips: a yes holds for every smaller candidate. Then a yes sends the search to the larger half. The range halves every round. Thirty rounds cover a billion candidates. Sources: [Wikipedia: Binary search](https://en.wikipedia.org/wiki/Binary_search), [Python docs: bisect](https://docs.python.org/3/library/bisect.html) - `range = 1 to 11` Eating piles [3, 6, 7, 11] within 8 hours. Speed is the answer. - `speed 6 takes 6 hours` Six hours is inside the limit. A slower speed might still fit. - `range = 1 to 5` Speed 6 is kept as the best so far. Now try the lower half. - `speed 3 takes 10 hours` That is over the limit. Everything below 3 is worse. - `speed 4 takes 8 hours` It fits exactly, and nothing smaller fits. The answer is 4. ## When should you use Binary search on the answer? Use Binary search on the answer when the problem statement says “minimize the maximum / maximize the minimum”, “smallest value that satisfies a condition” or “find the smallest capacity/speed/days that works”. A solution built on it usually runs in O(n log(range)). - minimize the maximum / maximize the minimum - smallest value that satisfies a condition - find the smallest capacity/speed/days that works - answer lies in a numeric range, not an array position - a feasibility check is monotonic (true after some point) ## What is Binary search on the answer confused with? Binary search on the answer is most often confused with Binary search (array), Greedy (exchange argument), Linear search and Dynamic programming (1-D). - **Binary search (array)** - There the range holds real stored data. Here it holds every value the answer might take. - **Greedy (exchange argument)** - The feasibility test is often greedy itself. The search only decides which candidate to test. - **Linear search** - Trying candidates one by one is correct but slow. A monotone test lets you skip half of them. - **Dynamic programming (1-D)** - DP builds the answer up from smaller answers. Here the answer is guessed and then checked. ## What is the time and space complexity of Binary search on the answer? The target complexity of Binary search on the answer is O(n log(range)). A range of 1e9 needs about 30 tests. Total cost is O(n log range) when one test costs O(n). ## A worked example of Binary search on the answer ### Ship every package within d days Packages must ship in the given order across d days. Pick the smallest daily capacity that still finishes in time. A package can never be split across two days. The answer lies between the largest single package and the sum of them all. For one candidate capacity, fill each day greedily and count the days. Compare that count against d. ```javascript function shipWithinDays(weights, days) { let lo = Math.max(...weights); // one day must hold the biggest package let hi = weights.reduce((a, b) => a + b, 0); // one day holds everything const fits = (capacity) => { let used = 1; let load = 0; for (const w of weights) { if (load + w > capacity) { used++; // start a new day load = 0; } load += w; } return used <= days; }; while (lo < hi) { const mid = lo + Math.floor((hi - lo) / 2); if (fits(mid)) hi = mid; else lo = mid + 1; } return lo; } ``` ## Common mistakes with Binary search on the answer - **Starting the range at zero** The low bound should be a value that could actually work. For a capacity that is the largest item. - **A test that is not monotone** If a larger candidate can fail after a smaller one passed, halving is wrong. Check the direction first. - **Losing the last candidate that worked** Either store it in a variable, or move high to mid rather than past it. - **Looping forever on a decimal answer** With real numbers lo never passes hi exactly. Run a fixed hundred rounds instead. ## Which interview problems use Binary search on the answer? Interview problems solved with Binary search on the answer include Koko eating bananas, Capacity to ship packages within D days, Split array largest sum, Minimum days to make m bouquets, Magnetic force between two balls, Find the smallest divisor given a threshold and Minimise max distance to gas station. - **Koko eating bananas** The candidate is a speed, the test counts hours. - **Capacity to ship packages within D days** The candidate is a daily load. - **Split array largest sum** The candidate is the largest sum any part may reach. - **Minimum days to make m bouquets** The candidate is a day, the test counts finished bouquets. - **Magnetic force between two balls** Maximise the smallest gap instead of minimising a maximum. - **Find the smallest divisor given a threshold** The candidate is the divisor. - **Minimise max distance to gas station** A decimal answer, so the loop runs a fixed count. ## JavaScript ```javascript function smallestFeasible(lo, hi, canDo) { while (lo < hi) { const mid = lo + ((hi - lo) >> 1); if (canDo(mid)) hi = mid; else lo = mid + 1; } return lo; } // canDo(x) must be monotonic: false...false, true...true ``` ## TypeScript ```typescript function smallestFeasible(lo: number, hi: number, canDo: (x: number) => boolean): number { while (lo < hi) { const mid = lo + ((hi - lo) >> 1); if (canDo(mid)) hi = mid; else lo = mid + 1; } return lo; } // canDo(x) must be monotonic: false...false, true...true ``` ## Python ```python def smallest_feasible(lo, hi, can_do): while lo < hi: mid = lo + (hi - lo) // 2 if can_do(mid): hi = mid else: lo = mid + 1 return lo # can_do(x) must be monotonic: False...False, True...True ``` ## PHP ```php function smallestFeasible(int $lo, int $hi, callable $canDo): int { while ($lo < $hi) { $mid = $lo + intdiv($hi - $lo, 2); if ($canDo($mid)) $hi = $mid; else $lo = $mid + 1; } return $lo; } // canDo($x) must be monotonic: false...false, true...true ``` ## Java ```java public static int smallestFeasible(int lo, int hi, IntPredicate canDo) { while (lo < hi) { int mid = lo + ((hi - lo) >>> 1); if (canDo.test(mid)) hi = mid; else lo = mid + 1; } return lo; } // canDo(x) must be monotonic: false...false, true...true public interface IntPredicate { boolean test(int x); } ``` ## C# ```csharp public static int SmallestFeasible(int lo, int hi, Func canDo) { while (lo < hi) { int mid = lo + (int)(((uint)(hi - lo)) >> 1); if (canDo(mid)) hi = mid; else lo = mid + 1; } return lo; } // canDo(x) must be monotonic: false...false, true...true ``` --- title: "Elementary sorts (selection, bubble, insertion)" url: https://algopath.pro/patterns/elementary-sort language: en summary: "Selection, bubble and insertion sort all compare neighbours and swap them. All three cost O(n squared) on a shuffled array." updated: 2026-10-17 --- # Elementary sorts (selection, bubble, insertion) Selection, bubble and insertion sort all compare neighbours and swap them. All three cost O(n squared) on a shuffled array. ## How does Elementary sorts (selection, bubble, insertion) work? All three keep a sorted part and an unsorted part. Each round moves one element across the line. Selection sort scans the unsorted part for the smallest value. It swaps that value into place. Bubble sort compares neighbouring pairs and swaps them when they are out of order. The largest value drifts to the end. Insertion sort takes the next element and slides it left. It stops as soon as the neighbour is smaller. Insertion sort is the one worth knowing. On nearly sorted data it does almost no work. All three sort in place with O(1) memory. Insertion and bubble are stable, selection is not. Sources: [Wikipedia: Insertion sort](https://en.wikipedia.org/wiki/Insertion_sort), [Wikipedia: Selection sort](https://en.wikipedia.org/wiki/Selection_sort) - `[5 | 2, 4, 1]` Insertion sort. The sorted part is the first element alone. - `[2, 5 | 4, 1]` 2 slides past 5. Two elements are now in order. - `[2, 4, 5 | 1]` 4 slides past 5 and stops at 2. - `[1, 2, 4, 5]` 1 slides all the way to the front. That is the worst move here. - `3 rounds` Nearly sorted input costs one comparison per element. No sliding happens at all. ## When should you use Elementary sorts (selection, bubble, insertion)? Use Elementary sorts (selection, bubble, insertion) when the problem statement says “n is small (tens or a few hundred)”, “array is nearly sorted already” or “sort in place with O(1) extra space”. A solution built on it usually runs in O(n^2). - n is small (tens or a few hundred) - array is nearly sorted already - sort in place with O(1) extra space - teaching/interview question about how sorting works - stability matters and simplicity is fine ## What is Elementary sorts (selection, bubble, insertion) confused with? Elementary sorts (selection, bubble, insertion) is most often confused with Fast sort (merge / quick), Non-comparison sort (counting / radix), Sort with a custom comparator and Binary heap / priority queue. - **Fast sort (merge / quick)** - Merge and quick sort split the array and cost n log n. These three never split anything. - **Non-comparison sort (counting / radix)** - Counting sort reads the values instead of comparing them. That needs a small range of keys. - **Sort with a custom comparator** - That page is about which order you want. This one is about how the order is produced. - **Binary heap / priority queue** - Selection sort scans for the smallest value every round. A heap hands it over in log n. ## What is the time and space complexity of Elementary sorts (selection, bubble, insertion)? The target complexity of Elementary sorts (selection, bubble, insertion) is O(n^2). n up to roughly 5000 is fine at O(n squared). Insertion sort drops to O(n) on nearly sorted data. ## A worked example of Elementary sorts (selection, bubble, insertion) ### Insertion sort on a linked list You get a singly linked list and have to sort it with insertion sort. Relinking the nodes is allowed. Copying the values into an array is not. Build a second, sorted list one node at a time. For each node, walk the sorted list from the front. Stop once the next value is larger and link it in. ```javascript function insertionSortList(head) { const dummy = new ListNode(0); let node = head; while (node) { const next = node.next; // walk from the front each time: the sorted part has no back links let prev = dummy; while (prev.next && prev.next.val < node.val) { prev = prev.next; } node.next = prev.next; prev.next = node; node = next; } return dummy.next; } ``` ## Common mistakes with Elementary sorts (selection, bubble, insertion) - **Reaching for bubble sort by default** Bubble sort does the most work of the three for the same cost class. Insertion sort is the better default. - **Sorting a large input this way** At n of 1e5 the square is 1e10 operations. Call the built-in sort instead. - **Scanning the inner loop to index zero** Insertion sort should stop once the left neighbour is smaller. Scanning further throws away its best case. - **Using selection sort where ties matter** A long swap can jump equal keys over each other. Insertion sort keeps their original order. ## Which interview problems use Elementary sorts (selection, bubble, insertion)? Interview problems solved with Elementary sorts (selection, bubble, insertion) include Sort an array, Insertion sort list, Sort colors, Sort a nearly sorted array, Height checker, Sort array by parity and First missing positive. - **Sort an array** Any of the three is enough below a few thousand elements. - **Insertion sort list** The same loop, over linked nodes instead of slots. - **Sort colors** Three values only, so one pass beats any comparison sort. - **Sort a nearly sorted array** Insertion sort is linear when each element is close to home. - **Height checker** Compare the row against a sorted copy of itself. - **Sort array by parity** A partition rather than a full sort. - **First missing positive** Cycle sort puts each value at its own index. ## JavaScript ```javascript function insertionSort(arr) { for (let i = 1; i < arr.length; i++) { const key = arr[i]; let j = i - 1; while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } return arr; } ``` ## TypeScript ```typescript function insertionSort(arr: number[]): number[] { for (let i = 1; i < arr.length; i++) { const key = arr[i]; let j = i - 1; while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } return arr; } ``` ## Python ```python def insertion_sort(arr): for i in range(1, len(arr)): key = arr[i] j = i - 1 while j >= 0 and arr[j] > key: arr[j + 1] = arr[j] j -= 1 arr[j + 1] = key return arr ``` ## PHP ```php function insertionSort(array $arr): array { for ($i = 1; $i < count($arr); $i++) { $key = $arr[$i]; $j = $i - 1; while ($j >= 0 && $arr[$j] > $key) { $arr[$j + 1] = $arr[$j]; $j--; } $arr[$j + 1] = $key; } return $arr; } ``` ## Java ```java public static int[] insertionSort(int[] arr) { for (int i = 1; i < arr.length; i++) { int key = arr[i]; int j = i - 1; while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } return arr; } ``` ## C# ```csharp public static int[] InsertionSort(int[] arr) { for (int i = 1; i < arr.Length; i++) { int key = arr[i]; int j = i - 1; while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } return arr; } ``` --- title: "Fast sort (merge / quick)" url: https://algopath.pro/patterns/fast-sort language: en summary: "Cut the array in two, sort each part, then put them back together. Merge sort cuts by position, quick sort cuts by value." updated: 2026-10-17 --- # Fast sort (merge / quick) Cut the array in two, sort each part, then put them back together. Merge sort cuts by position, quick sort cuts by value. ## How does Fast sort (merge / quick) work? Both sorts cut one problem into two smaller ones. They differ in where the cut falls. Merge sort cuts at the midpoint, without looking at any value. Each half is then sorted the same way. Merging two sorted halves is one linear pass. Take whichever front element is smaller. Quick sort picks a pivot value and partitions around it. Smaller values go left, larger ones go right. After a partition the pivot sits in its final slot. Each side is then sorted on its own. Both do log n levels of work over n elements. A bad pivot makes quick sort O(n squared). Sources: [Wikipedia: Merge sort](https://en.wikipedia.org/wiki/Merge_sort), [Wikipedia: Quicksort](https://en.wikipedia.org/wiki/Quicksort), [Hoare (1962), Quicksort](https://doi.org/10.1093/comjnl/5.1.10) - `[3, 1, 4, 2]` Merge sort. Cut at the midpoint, ignoring the values. - `[3, 1] and [4, 2]` Each half is cut again, down to single elements. - `[1, 3] and [2, 4]` A single element is already sorted. Each pair merges in one comparison. - `[1, 2, ...]` The merge compares the two fronts. 1 is smaller than 2, so it goes first. - `[1, 2, 3, 4]` The last merge is one pass over four elements. Two levels in total. ## When should you use Fast sort (merge / quick)? Use Fast sort (merge / quick) when the problem statement says “sort an array of real size (thousands+)”, “need O(n log n) guaranteed or expected” or “sort as a subroutine before another algorithm (two pointers, greedy, sweep)”. A solution built on it usually runs in O(n log n). - sort an array of real size (thousands+) - need O(n log n) guaranteed or expected - sort as a subroutine before another algorithm (two pointers, greedy, sweep) - stability required (merge sort) or in-place expected (quicksort) - just "sort this" with no small-n or special-key hint ## What is Fast sort (merge / quick) confused with? Fast sort (merge / quick) is most often confused with Elementary sorts (selection, bubble, insertion), Non-comparison sort (counting / radix), Binary heap / priority queue and Recursion. - **Elementary sorts (selection, bubble, insertion)** - Those never split the array and cost n squared. Splitting is what buys the log n. - **Non-comparison sort (counting / radix)** - Counting and radix sort beat n log n by reading digits. They need keys in a small range. - **Binary heap / priority queue** - Heap sort also runs in n log n and needs no buffer. It is slower in practice on real data. - **Recursion** - Recursion is the mechanism these use, not the pattern. Where the cut goes is the real idea. ## What is the time and space complexity of Fast sort (merge / quick)? The target complexity of Fast sort (merge / quick) is O(n log n). n up to 1e6 gives O(n log n). Merge sort needs O(n) extra memory, quick sort needs O(log n). ## A worked example of Fast sort (merge / quick) ### The kth largest value Return the kth largest value in an unsorted array. Sorting the whole array works, but it does far more than the question asked. Partition around a pivot, exactly as quick sort does. The pivot then sits in its final slot. Only the side that contains position k needs another round. ```javascript function findKthLargest(nums, k) { const target = nums.length - k; // the slot it would hold once sorted let lo = 0; let hi = nums.length - 1; while (lo < hi) { const pivot = nums[hi]; let split = lo; for (let i = lo; i < hi; i++) { if (nums[i] < pivot) { [nums[i], nums[split]] = [nums[split], nums[i]]; split++; } } [nums[split], nums[hi]] = [nums[hi], nums[split]]; // the pivot is final, and only one side can hold the answer if (split === target) return nums[split]; if (split < target) lo = split + 1; else hi = split - 1; } return nums[lo]; } ``` ## Common mistakes with Fast sort (merge / quick) - **Always taking the first element as pivot** Sorted input then splits into one and n minus one. Pick a random or middle element. - **Breaking ties toward the right half** Taking the right element on a tie reorders equal keys. Prefer the left half when they match. - **Recursing on both sides of a quickselect** Only one side can hold the position you want. Visiting both costs a full sort. - **Forgetting merge sort's buffer** Merge sort allocates a second array the size of the input. On very large arrays that matters. ## Which interview problems use Fast sort (merge / quick)? Interview problems solved with Fast sort (merge / quick) include Sort an array, Kth largest element in an array, Merge sorted array, Count of smaller numbers after self, Reverse pairs, Sort list and Wiggle sort II. - **Sort an array** The plain form, usually written as merge sort. - **Kth largest element in an array** One partition per round instead of a full sort. - **Merge sorted array** The merge half of merge sort, on its own. - **Count of smaller numbers after self** Counted while the merge step runs. - **Reverse pairs** The same counting trick with a different comparison. - **Sort list** Merge sort over linked nodes, with no buffer. - **Wiggle sort II** Quickselect finds the median, then values are placed around it. ## JavaScript ```javascript function mergeSort(arr) { if (arr.length <= 1) return arr; const mid = arr.length >> 1; const left = mergeSort(arr.slice(0, mid)); const right = mergeSort(arr.slice(mid)); const out = []; let i = 0, j = 0; while (i < left.length && j < right.length) { out.push(left[i] <= right[j] ? left[i++] : right[j++]); } return out.concat(left.slice(i), right.slice(j)); } ``` ## TypeScript ```typescript function mergeSort(arr: number[]): number[] { if (arr.length <= 1) return arr; const mid = arr.length >> 1; const left = mergeSort(arr.slice(0, mid)); const right = mergeSort(arr.slice(mid)); const out: number[] = []; let i = 0, j = 0; while (i < left.length && j < right.length) { out.push(left[i] <= right[j] ? left[i++] : right[j++]); } return out.concat(left.slice(i), right.slice(j)); } ``` ## Python ```python def merge_sort(arr): if len(arr) <= 1: return arr mid = len(arr) // 2 left, right = merge_sort(arr[:mid]), merge_sort(arr[mid:]) out, i, j = [], 0, 0 while i < len(left) and j < len(right): if left[i] <= right[j]: out.append(left[i]); i += 1 else: out.append(right[j]); j += 1 return out + left[i:] + right[j:] ``` ## PHP ```php function mergeSort(array $arr): array { if (count($arr) <= 1) return $arr; $mid = intdiv(count($arr), 2); $left = mergeSort(array_slice($arr, 0, $mid)); $right = mergeSort(array_slice($arr, $mid)); $out = []; $i = 0; $j = 0; while ($i < count($left) && $j < count($right)) { $out[] = $left[$i] <= $right[$j] ? $left[$i++] : $right[$j++]; } return array_merge($out, array_slice($left, $i), array_slice($right, $j)); } ``` ## Java ```java public static int[] mergeSort(int[] arr) { if (arr.length <= 1) return arr; int mid = arr.length >>> 1; int[] left = mergeSort(Arrays.copyOfRange(arr, 0, mid)); int[] right = mergeSort(Arrays.copyOfRange(arr, mid, arr.length)); int[] out = new int[arr.length]; int i = 0, j = 0, k = 0; while (i < left.length && j < right.length) { out[k++] = left[i] <= right[j] ? left[i++] : right[j++]; } while (i < left.length) out[k++] = left[i++]; while (j < right.length) out[k++] = right[j++]; return out; } ``` ## C# ```csharp public static int[] MergeSort(int[] arr) { if (arr.Length <= 1) return arr; int mid = arr.Length >> 1; int[] left = MergeSort(arr[..mid]); int[] right = MergeSort(arr[mid..]); var outArr = new int[arr.Length]; int i = 0, j = 0, k = 0; while (i < left.Length && j < right.Length) { outArr[k++] = left[i] <= right[j] ? left[i++] : right[j++]; } while (i < left.Length) outArr[k++] = left[i++]; while (j < right.Length) outArr[k++] = right[j++]; return outArr; } ``` --- title: "Non-comparison sort (counting / radix)" url: https://algopath.pro/patterns/non-comparison-sort language: en summary: "Counting and radix sort never compare two values. They read the key itself, which beats n log n when the range is small." updated: 2026-10-17 --- # Non-comparison sort (counting / radix) Counting and radix sort never compare two values. They read the key itself, which beats n log n when the range is small. ## How does Non-comparison sort (counting / radix) work? Counting sort needs keys from a known, small range. Make one counter for each possible key. Walk the input once and tally every key. Now you know how many of each exist. Turn those tallies into starting positions. A running total says where each key's block begins. Walk the input again and drop each item at its position. Nothing was ever compared. Radix sort applies the same idea one digit at a time. It starts from the least significant digit. Every digit pass has to be stable. An unstable pass undoes the passes before it. Sources: [Wikipedia: Counting sort](https://en.wikipedia.org/wiki/Counting_sort), [Wikipedia: Radix sort](https://en.wikipedia.org/wiki/Radix_sort) - `counts = [1, 1, 2]` Sorting [2, 0, 2, 1]. One zero, one one, two twos. - `starts = [0, 1, 2]` Running totals give each key its first slot. - `[0, _, _, _]` The 0 lands in slot 0. Its start moves on by one. - `[0, 1, 2, _]` The 1 takes slot 1, and the first 2 takes slot 2. - `[0, 1, 2, 2]` The second 2 takes slot 3. Not one comparison happened. ## When should you use Non-comparison sort (counting / radix)? Use Non-comparison sort (counting / radix) when the problem statement says “keys are integers in a small known range”, “sort ages, grades, or bounded counts” or “need O(n) and comparisons are the bottleneck”. A solution built on it usually runs in O(n). - keys are integers in a small known range - sort ages, grades, or bounded counts - need O(n) and comparisons are the bottleneck - sort by digit or fixed-width key - n large but the value range is much smaller than n ## What is Non-comparison sort (counting / radix) confused with? Non-comparison sort (counting / radix) is most often confused with Fast sort (merge / quick), Hash set / map, Sort with a custom comparator and Elementary sorts (selection, bubble, insertion). - **Fast sort (merge / quick)** - No comparison sort can beat n log n. Reading the key steps around that limit entirely. - **Hash set / map** - A map counts values too, but it keeps no order. Counting sort uses the value as an index. - **Sort with a custom comparator** - A comparator describes the order between two items. Counting sort never looks at two items. - **Elementary sorts (selection, bubble, insertion)** - Those compare neighbours and cost n squared. Counting sort is linear when the range allows it. ## What is the time and space complexity of Non-comparison sort (counting / radix)? The target complexity of Non-comparison sort (counting / radix) is O(n). n values with keys under k gives O(n + k). Radix sort costs O(d times n) across d digits. ## A worked example of Non-comparison sort (counting / radix) ### The k most frequent values Return the k values that appear most often in an array. Sorting the counts costs n log n. The question never asked for a full order. Count every value with a map. A count can never exceed the array length. Use the count itself as an index into a list of buckets. Read the buckets from the back. ```javascript function topKFrequent(nums, k) { const count = new Map(); for (const x of nums) count.set(x, (count.get(x) ?? 0) + 1); // bucket i holds every value that appeared exactly i times const buckets = Array.from({ length: nums.length + 1 }, () => []); for (const [value, c] of count) buckets[c].push(value); const answer = []; for (let c = buckets.length - 1; c >= 1 && answer.length < k; c--) { for (const value of buckets[c]) { answer.push(value); if (answer.length === k) break; } } return answer; } ``` ## Common mistakes with Non-comparison sort (counting / radix) - **Using it on a huge range of keys** One counter per key means memory the size of the range. Values up to 1e9 make that impossible. - **An unstable pass inside radix sort** Each digit pass must preserve the order from the last one. Otherwise earlier work is lost. - **Forgetting negative values** An array index cannot be negative. Shift every key by the minimum first. - **Counting floats or strings** The key has to be a bounded integer. Anything else needs a comparison sort. ## Which interview problems use Non-comparison sort (counting / radix)? Interview problems solved with Non-comparison sort (counting / radix) include Sort an array of small integers, Sort colors, Top k frequent elements, H-index, Maximum gap, Relative sort array and Sort characters by frequency. - **Sort an array of small integers** One counter per value, then read them back in order. - **Sort colors** Three possible keys, so three counters. - **Top k frequent elements** The count itself becomes the bucket index. - **H-index** Citation counts, capped at the number of papers. - **Maximum gap** Radix sort is what makes a linear solution possible. - **Relative sort array** Counting sort with an order given by another list. - **Sort characters by frequency** Buckets keyed by how often a letter appears. ## JavaScript ```javascript function countingSort(arr, maxVal) { const counts = new Array(maxVal + 1).fill(0); for (const x of arr) counts[x]++; const out = []; for (let v = 0; v <= maxVal; v++) { while (counts[v]-- > 0) out.push(v); } return out; } ``` ## TypeScript ```typescript function countingSort(arr: number[], maxVal: number): number[] { const counts: number[] = new Array(maxVal + 1).fill(0); for (const x of arr) counts[x]++; const out: number[] = []; for (let v = 0; v <= maxVal; v++) { while (counts[v]-- > 0) out.push(v); } return out; } ``` ## Python ```python def counting_sort(arr, max_val): counts = [0] * (max_val + 1) for x in arr: counts[x] += 1 out = [] for v, c in enumerate(counts): out.extend([v] * c) return out ``` ## PHP ```php function countingSort(array $arr, int $maxVal): array { $counts = array_fill(0, $maxVal + 1, 0); foreach ($arr as $x) $counts[$x]++; $out = []; for ($v = 0; $v <= $maxVal; $v++) { for ($c = $counts[$v]; $c > 0; $c--) $out[] = $v; } return $out; } ``` ## Java ```java public static int[] countingSort(int[] arr, int maxVal) { int[] counts = new int[maxVal + 1]; for (int x : arr) counts[x]++; int[] out = new int[arr.length]; int k = 0; for (int v = 0; v <= maxVal; v++) { while (counts[v]-- > 0) out[k++] = v; } return out; } ``` ## C# ```csharp public static int[] CountingSort(int[] arr, int maxVal) { var counts = new int[maxVal + 1]; foreach (int x in arr) counts[x]++; var outArr = new int[arr.Length]; int k = 0; for (int v = 0; v <= maxVal; v++) { while (counts[v]-- > 0) outArr[k++] = v; } return outArr; } ``` --- title: "Sort with a custom comparator" url: https://algopath.pro/patterns/sort-comparators language: en summary: "A comparator answers exactly one question: which of these two items comes first. The sort algorithm handles everything else." updated: 2026-10-17 --- # Sort with a custom comparator A comparator answers exactly one question: which of these two items comes first. The sort algorithm handles everything else. ## How does Sort with a custom comparator work? A comparator takes two items and returns a number. Negative means the first one comes earlier. Zero means the two are tied. A stable sort then leaves them in their original order. For plain numbers, return a minus b to go ascending. Avoid the subtraction when values can overflow. For several keys, compare the first one. Fall through to the next key only on a tie. The rule has to be consistent. If a beats b and b beats c, then a beats c. The comparator runs n log n times. Anything expensive inside it should be computed once beforehand. Sources: [Python docs: Sorting techniques](https://docs.python.org/3/howto/sorting.html), [MDN: Array.prototype.sort()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) - `[Ann 30, Bo 25, Cy 30]` Sort by age first, then by name. - `compare(Ann, Bo)` 30 against 25 returns positive. Bo belongs earlier. - `compare(Ann, Cy)` The ages tie at 30. The rule falls through to the name. - `Ann before Cy` The second key decides it. Ann wins on the name. - `[Bo 25, Ann 30, Cy 30]` Two keys, one pass. The order is now total. ## When should you use Sort with a custom comparator? Use Sort with a custom comparator when the problem statement says “sort by multiple fields / tie-break on a second key”, “custom order (not plain ascending)” or “sort descending, or by a computed key”. A solution built on it usually runs in O(n log n). - sort by multiple fields / tie-break on a second key - custom order (not plain ascending) - sort descending, or by a computed key - "sort so that X comes before Y when..." - reorder objects/records, not raw numbers ## What is Sort with a custom comparator confused with? Sort with a custom comparator is most often confused with Fast sort (merge / quick), Greedy (exchange argument), Intervals: merge & insert and Binary heap / priority queue. - **Fast sort (merge / quick)** - That page is about how the sorting happens. This one is about the order you ask for. - **Greedy (exchange argument)** - A greedy proof usually decides which order is correct. The comparator is how you write it down. - **Intervals: merge & insert** - Interval work starts with a sort by start time. The merging is a separate step after it. - **Binary heap / priority queue** - A heap takes the same comparator but keeps only the front. Use it when data is still arriving. ## What is the time and space complexity of Sort with a custom comparator? The target complexity of Sort with a custom comparator is O(n log n). n up to 1e6 gives O(n log n) comparisons. A slow comparator multiplies that, so precompute the key. ## A worked example of Sort with a custom comparator ### Arrange numbers into the largest one You get a list of non-negative integers. Arrange them so the joined digits read as the largest number. Return a string, because the result can be huge. Sorting by value is wrong here, since 9 must come before 30. Compare two numbers by the two strings they can form. Put a first when a plus b reads larger. ```javascript function largestNumber(nums) { const parts = nums.map(String); // whichever joined order reads larger wins the comparison parts.sort((a, b) => (b + a).localeCompare(a + b)); if (parts[0] === "0") return "0"; // every value was zero return parts.join(""); } ``` ## Common mistakes with Sort with a custom comparator - **Sorting numbers with no comparator** JavaScript compares as strings by default, so 10 lands before 9. Always pass a comparator. - **Subtracting very large values** The difference can overflow or lose precision. Return minus one, zero or one instead. - **Writing a rule that contradicts itself** An inconsistent comparator gives an undefined result. Some engines throw instead of sorting. - **Computing an expensive key inside it** The comparator runs on every comparison, not once per item. Build the key first, then sort by it. ## Which interview problems use Sort with a custom comparator? Interview problems solved with Sort with a custom comparator include Largest number, Merge intervals, Meeting rooms, Sort by increasing frequency, Custom sort string, Relative sort array and K closest points to origin. - **Largest number** Order by which of the two joined strings reads larger. - **Merge intervals** Sort by start time before anything else happens. - **Meeting rooms** The same sort, then a check on each neighbouring pair. - **Sort by increasing frequency** Frequency is the first key, the value is the second. - **Custom sort string** The order comes from another string entirely. - **Relative sort array** Listed values first, everything else ascending. - **K closest points to origin** Sort by the squared distance, never the square root. ## JavaScript ```javascript // sort by a custom / multi-key order items.sort((a, b) => { if (a.priority !== b.priority) return a.priority - b.priority; return a.name.localeCompare(b.name); }); ``` ## TypeScript ```typescript interface Item { priority: number; name: string; } // sort by a custom / multi-key order function sortItems(items: Item[]): void { items.sort((a, b) => { if (a.priority !== b.priority) return a.priority - b.priority; return a.name.localeCompare(b.name); }); } ``` ## Python ```python from functools import cmp_to_key def compare(a, b): if a["priority"] != b["priority"]: return a["priority"] - b["priority"] return -1 if a["name"] < b["name"] else 1 items.sort(key=cmp_to_key(compare)) # or simply: items.sort(key=lambda x: (x["priority"], x["name"])) ``` ## PHP ```php usort($items, function ($a, $b) { if ($a["priority"] !== $b["priority"]) { return $a["priority"] <=> $b["priority"]; } return strcmp($a["name"], $b["name"]); }); ``` ## Java ```java // sort by a custom / multi-key order public static void sortItems(List items) { items.sort(Comparator .comparingInt((Item a) -> a.priority) .thenComparing(a -> a.name)); } public static class Item { public int priority; public String name; } ``` ## C# ```csharp // sort by a custom / multi-key order public static void SortItems(List items) { items.Sort((a, b) => a.Priority != b.Priority ? a.Priority.CompareTo(b.Priority) : string.CompareOrdinal(a.Name, b.Name)); } public class Item { public int Priority; public string Name = ""; } ``` --- title: "Intervals: merge & insert" url: https://algopath.pro/patterns/intervals-merge language: en summary: "Sort the intervals by start, then walk them once. Two neighbours merge whenever the next start is not past the current end." updated: 2026-10-15 --- # Intervals: merge & insert Sort the intervals by start, then walk them once. Two neighbours merge whenever the next start is not past the current end. ## How does Intervals: merge & insert work? Sort the intervals by their start value. Nothing below works without that order. Take the first interval as the current block. Everything after it is compared against this block. Look at the next interval and read its start. Compare that start against the current end. If the start is not past the current end, the two touch. Widen the current end to the larger of the two. If the start is past it, the block is finished. Push it and make the new interval current. One pass covers the whole list. The last block is pushed after the loop ends. - `current = [1, 3]` The list is sorted by start. The first interval opens a block. - `next = [2, 6]` 2 is not past 3, so they overlap. The end grows to 6. - `current = [1, 6], next = [8, 10]` 8 is past 6. The block is finished and pushed. - `current = [8, 10], next = [9, 12]` 9 is not past 10. The end grows to 12. - `[[1, 6], [8, 12]]` The last block is pushed after the loop. Four intervals became two. ## When should you use Intervals: merge & insert? Use Intervals: merge & insert when the problem statement says “merge overlapping intervals”, “insert a new interval into a sorted list” or “meetings, bookings, or ranges that overlap”. A solution built on it usually runs in O(n log n). - merge overlapping intervals - insert a new interval into a sorted list - meetings, bookings, or ranges that overlap - given as [start, end] pairs - free time / busy time between intervals ## What is Intervals: merge & insert confused with? Intervals: merge & insert is most often confused with Sweep line (event counting), Greedy (exchange argument), Sort with a custom comparator and Difference array. - **Sweep line (event counting)** - A sweep breaks each interval into two events and counts. Merging keeps the intervals whole. - **Greedy (exchange argument)** - Removing the fewest intervals is a greedy choice by end time. Merging only joins what overlaps. - **Sort with a custom comparator** - The sort by start is the setup step. This page is about the pass that follows it. - **Difference array** - That counts how deep the overlap runs at each index. Merging returns intervals, not counts. ## What is the time and space complexity of Intervals: merge & insert? The target complexity of Intervals: merge & insert is O(n log n). n up to 1e6 costs O(n log n), all of it in the sort. The pass afterwards is O(n). ## A worked example of Intervals: merge & insert ### Insert one interval into a sorted list You get a sorted list of intervals that do not overlap, plus one new interval. Insert it, merge whatever it touches, and keep the result sorted and disjoint. Copy across every interval that ends before the new one starts. Then absorb every interval that overlaps, widening the new one. Copy the remaining tail unchanged. ```javascript function insert(intervals, newInterval) { const result = []; let [start, end] = newInterval; let i = 0; while (i < intervals.length && intervals[i][1] < start) { result.push(intervals[i]); // ends before the new one begins i++; } while (i < intervals.length && intervals[i][0] <= end) { start = Math.min(start, intervals[i][0]); end = Math.max(end, intervals[i][1]); // the later interval may end sooner i++; } result.push([start, end]); while (i < intervals.length) { result.push(intervals[i]); i++; } return result; } ``` ## Common mistakes with Intervals: merge & insert - **Skipping the sort** The single pass assumes the starts only grow. Unsorted input merges the wrong pairs. - **Guessing the overlap comparison** Whether [1, 2] and [2, 3] touch is the problem's choice. Read the statement before picking the operator. - **Forgetting the final block** The current interval is pushed only after the loop. Without that the answer loses one. - **Taking the end from the later interval** A later interval can end sooner than the current one. The new end is the maximum of both. ## Which interview problems use Intervals: merge & insert? Interview problems solved with Intervals: merge & insert include Merge intervals, Insert interval, Non-overlapping intervals, Meeting rooms, Interval list intersections, Employee free time and Remove covered intervals. - **Merge intervals** The plain form: sort by start, then one pass. - **Insert interval** The list is already sorted, so only the middle needs merging. - **Non-overlapping intervals** Keep the interval that finishes first, greedily. - **Meeting rooms** Any overlap at all makes the answer false. - **Interval list intersections** Two sorted lists walked with two pointers. - **Employee free time** Merge every busy block, then read the gaps between. - **Remove covered intervals** Sort by start, and on a tie put the longer one first. ## JavaScript ```javascript function mergeIntervals(intervals) { intervals.sort((a, b) => a[0] - b[0]); const result = []; for (const [start, end] of intervals) { const last = result[result.length - 1]; if (last && start <= last[1]) { last[1] = Math.max(last[1], end); } else { result.push([start, end]); } } return result; } ``` ## TypeScript ```typescript function mergeIntervals(intervals: number[][]): number[][] { intervals.sort((a, b) => a[0] - b[0]); const result: number[][] = []; for (const [start, end] of intervals) { const last = result[result.length - 1]; if (last && start <= last[1]) { last[1] = Math.max(last[1], end); } else { result.push([start, end]); } } return result; } ``` ## Python ```python def merge_intervals(intervals): intervals.sort(key=lambda x: x[0]) result = [] for start, end in intervals: if result and start <= result[-1][1]: result[-1][1] = max(result[-1][1], end) else: result.append([start, end]) return result ``` ## PHP ```php function mergeIntervals(array $intervals): array { usort($intervals, fn($a, $b) => $a[0] <=> $b[0]); $result = []; foreach ($intervals as [$start, $end]) { $lastIdx = count($result) - 1; if ($lastIdx >= 0 && $start <= $result[$lastIdx][1]) { $result[$lastIdx][1] = max($result[$lastIdx][1], $end); } else { $result[] = [$start, $end]; } } return $result; } ``` ## Java ```java public static List mergeIntervals(int[][] intervals) { Arrays.sort(intervals, Comparator.comparingInt(a -> a[0])); List result = new ArrayList<>(); for (int[] interval : intervals) { int[] last = result.isEmpty() ? null : result.get(result.size() - 1); if (last != null && interval[0] <= last[1]) { last[1] = Math.max(last[1], interval[1]); } else { result.add(new int[] { interval[0], interval[1] }); } } return result; } ``` ## C# ```csharp public static List MergeIntervals(int[][] intervals) { Array.Sort(intervals, (a, b) => a[0].CompareTo(b[0])); var result = new List(); foreach (var interval in intervals) { var last = result.Count > 0 ? result[^1] : null; if (last != null && interval[0] <= last[1]) { last[1] = Math.Max(last[1], interval[1]); } else { result.Add(new int[] { interval[0], interval[1] }); } } return result; } ``` --- title: "Sweep line (event counting)" url: https://algopath.pro/patterns/sweep-line language: en summary: "Turn each interval into a start event and an end event. Then sort all the events by position and walk them with a counter." updated: 2026-10-17 --- # Sweep line (event counting) Turn each interval into a start event and an end event. Then sort all the events by position and walk them with a counter. ## How does Sweep line (event counting) work? Each interval becomes two events. One start at the left coordinate, one end at the right. Put every event into a single list. A start carries plus one, an end carries minus one. Sort that list by coordinate. Ties need a rule, and the rule depends on the question. Walk the sorted events and keep a running counter. It always holds how many intervals are open. Record whatever the question asks at each step. Usually that is the highest the counter reached. The coordinates never have to be dense. Only their relative order matters. Sources: [Wikipedia: Sweep line algorithm](https://en.wikipedia.org/wiki/Sweep_line_algorithm) - `meetings [1, 4], [2, 5], [7, 9]` Three meetings become six separate events. - `1+, 2+, 4-, 5-, 7+, 9-` The events are sorted by time. The intervals no longer exist. - `count = 1 at time 1` The first meeting opens. One room is in use. - `count = 2 at time 2` The second opens before the first ends. Two rooms are needed. - `count = 0 at time 5` Both close before 7 arrives. The peak was 2. ## When should you use Sweep line (event counting)? Use Sweep line (event counting) when the problem statement says “maximum number of overlapping meetings/intervals at once”, “minimum rooms/resources needed” or “busiest moment in time”. A solution built on it usually runs in O(n log n). - maximum number of overlapping meetings/intervals at once - minimum rooms/resources needed - busiest moment in time - how many intervals cover a given point - events happening at the same time ## What is Sweep line (event counting) confused with? Sweep line (event counting) is most often confused with Intervals: merge & insert, Difference array, Binary heap / priority queue and Prefix sums. - **Intervals: merge & insert** - Merging keeps intervals whole and joins the ones that touch. A sweep forgets them and counts. - **Difference array** - That is a sweep whose coordinates are array indices. It needs a small, dense range. - **Binary heap / priority queue** - A heap holds the open intervals so you know which ends soonest. The counter only knows how many. - **Prefix sums** - Prefix sums answer over fixed positions in an array. A sweep walks events in sorted order. ## What is the time and space complexity of Sweep line (event counting)? The target complexity of Sweep line (event counting) is O(n log n). n intervals give 2n events, so O(n log n) for the sort. The walk itself is O(n). ## A worked example of Sweep line (event counting) ### How many meeting rooms are needed You get the start and end time of every meeting. Find the smallest number of rooms that holds them all. Two meetings need separate rooms when they overlap. Sort the start times and the end times into two lists. Walk them together: a start takes a room, an end gives one back. The peak count is the answer. ```javascript function minMeetingRooms(intervals) { const starts = intervals.map((i) => i[0]).sort((a, b) => a - b); const ends = intervals.map((i) => i[1]).sort((a, b) => a - b); let rooms = 0; let best = 0; let e = 0; for (const start of starts) { // every meeting already finished hands its room back first while (ends[e] <= start) { rooms--; e++; } rooms++; best = Math.max(best, rooms); } return best; } ``` ## Common mistakes with Sweep line (event counting) - **No rule for events at the same coordinate** Does an end at time 5 free a room for a start at 5? Decide that, then encode it. - **Sorting the intervals instead of the events** A sweep needs starts and ends interleaved. Sorting whole intervals keeps each pair glued together. - **Reading the counter after the walk** The answer is normally the peak, not the final value. The final value is always zero, because every start has a matching end. - **Indexing an array by the coordinate** Timestamps up to 1e9 cannot be array indices. Sort the events instead of allocating. ## Which interview problems use Sweep line (event counting)? Interview problems solved with Sweep line (event counting) include Meeting rooms II, Car pooling, My calendar III, Number of flowers in full bloom, The skyline problem, Employee free time and Maximum population year. - **Meeting rooms II** The peak counter is the number of rooms. - **Car pooling** Passengers get on and off along one route. - **My calendar III** The counter must never pass the booking limit. - **Number of flowers in full bloom** Count how many ranges cover each query day. - **The skyline problem** A sweep with a heap holding the live heights. - **Employee free time** Every gap where the counter sits at zero. - **Maximum population year** Births and deaths as events along a timeline. ## JavaScript ```javascript function maxOverlap(intervals) { const events = []; for (const [start, end] of intervals) { events.push([start, 1]); events.push([end, -1]); } events.sort((a, b) => a[0] - b[0] || a[1] - b[1]); let active = 0; let best = 0; for (const [, delta] of events) { active += delta; best = Math.max(best, active); } return best; } ``` ## TypeScript ```typescript function maxOverlap(intervals: number[][]): number { const events: number[][] = []; for (const [start, end] of intervals) { events.push([start, 1]); events.push([end, -1]); } events.sort((a, b) => a[0] - b[0] || a[1] - b[1]); let active = 0; let best = 0; for (const [, delta] of events) { active += delta; best = Math.max(best, active); } return best; } ``` ## Python ```python def max_overlap(intervals): events = [] for start, end in intervals: events.append((start, 1)) events.append((end, -1)) events.sort() active = 0 best = 0 for _, delta in events: active += delta best = max(best, active) return best ``` ## PHP ```php function maxOverlap(array $intervals): int { $events = []; foreach ($intervals as [$start, $end]) { $events[] = [$start, 1]; $events[] = [$end, -1]; } usort($events, fn($a, $b) => $a[0] <=> $b[0] ?: $a[1] <=> $b[1]); $active = 0; $best = 0; foreach ($events as [, $delta]) { $active += $delta; $best = max($best, $active); } return $best; } ``` ## Java ```java public static int maxOverlap(int[][] intervals) { List events = new ArrayList<>(); for (int[] interval : intervals) { events.add(new int[] { interval[0], 1 }); events.add(new int[] { interval[1], -1 }); } events.sort(Comparator .comparingInt((int[] e) -> e[0]) .thenComparingInt(e -> e[1])); int active = 0; int best = 0; for (int[] event : events) { active += event[1]; best = Math.max(best, active); } return best; } ``` ## C# ```csharp public static int MaxOverlap(int[][] intervals) { var events = new List(); foreach (var interval in intervals) { events.Add(new int[] { interval[0], 1 }); events.Add(new int[] { interval[1], -1 }); } events.Sort((a, b) => a[0] != b[0] ? a[0].CompareTo(b[0]) : a[1].CompareTo(b[1])); int active = 0; int best = 0; foreach (var e in events) { active += e[1]; best = Math.Max(best, active); } return best; } ``` --- title: "Linked list reversal" url: https://algopath.pro/patterns/linked-list-reversal language: en summary: "Walk the list once, and at every node flip its next pointer to the node behind it. Three local variables are all you need." updated: 2026-10-17 --- # Linked list reversal Walk the list once, and at every node flip its next pointer to the node behind it. Three local variables are all you need. ## How does Linked list reversal work? Keep three references: previous, current, and the one after current. previous starts as null. Save current.next before touching anything. Otherwise the rest of the list is lost. Point current.next at previous. That single line is the whole reversal. Move previous to current, and current to the saved node. The window slides forward by one. Stop when current is null. previous now points at the old last node. Return previous rather than current. The old head has become the tail. Sources: [Wikipedia: Linked list](https://en.wikipedia.org/wiki/Linked_list) - `prev = null, cur = 1` The list reads 1, 2, 3. Nothing is reversed yet. - `1 points at null, cur = 2` Node 1 now points at null. It is the new tail. - `2 points at 1, cur = 3` Node 2 points back at node 1. Two nodes are done. - `3 points at 2, cur = null` The last node flips. current has run off the end. - `head = 3` prev holds the new head. Three flips in one pass. ## When should you use Linked list reversal? Use Linked list reversal when the problem statement says “reverse a linked list (whole list or between positions)”, “reverse in groups of k” or “no extra array or O(1) extra space allowed”. A solution built on it usually runs in O(n). - reverse a linked list (whole list or between positions) - reverse in groups of k - no extra array or O(1) extra space allowed - swap pairs of nodes - singly linked list, only .next available ## What is Linked list reversal confused with? Linked list reversal is most often confused with Linked list merge & reorder, Fast & slow pointers, Stack (LIFO) and Recursion. - **Linked list merge & reorder** - Merging weaves two lists into one. Reversal rewires a single list in place. - **Fast & slow pointers** - That pair finds a midpoint or a cycle by walking. It never changes a link. - **Stack (LIFO)** - Pushing every node and popping it also reverses the order. That costs O(n) memory. - **Recursion** - The recursive version reads well but uses n stack frames. The loop uses three variables. ## What is the time and space complexity of Linked list reversal? The target complexity of Linked list reversal is O(n). n up to 1e6 gives O(n) time and O(1) space. The recursive form instead costs O(n) stack. ## A worked example of Linked list reversal ### Reverse only part of a list Reverse the nodes from position left to position right. Everything outside that range keeps its order. One pass is expected, and copying values is not allowed. Walk to the node just before left and hold on to it. Call that node the anchor. Then pull each following node to the front of the reversed part. The anchor keeps the list joined. ```javascript function reverseBetween(head, left, right) { const dummy = new ListNode(0, head); let anchor = dummy; for (let i = 1; i < left; i++) anchor = anchor.next; const tail = anchor.next; // this node ends up last in the reversed part for (let i = 0; i < right - left; i++) { const moved = tail.next; tail.next = moved.next; moved.next = anchor.next; anchor.next = moved; } return dummy.next; } ``` ## Common mistakes with Linked list reversal - **Losing the rest of the list** Overwriting current.next before saving it drops every later node. Save it first, every time. - **Returning the wrong node** current is null once the loop ends. The new head is previous. - **Leaving the old head pointing somewhere** The old head has to end up pointing at null. Starting previous at null does that for free. - **Using recursion on a long list** A million nodes means a million stack frames. That overflows the call stack. ## Which interview problems use Linked list reversal? Interview problems solved with Linked list reversal include Reverse linked list, Reverse linked list II, Reverse nodes in k-group, Palindrome linked list, Reorder list, Swap nodes in pairs and Add two numbers II. - **Reverse linked list** The plain form, with three variables. - **Reverse linked list II** Only a section flips, so an anchor is needed. - **Reverse nodes in k-group** Reverse each block of k, then join the blocks. - **Palindrome linked list** Reverse the second half and compare it with the first. - **Reorder list** Split it, reverse the back half, then weave the two. - **Swap nodes in pairs** Reversal with k fixed at two. - **Add two numbers II** Reverse both lists, add them, reverse the result. ## JavaScript ```javascript function reverseList(head) { let prev = null; let curr = head; while (curr) { const next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; } ``` ## TypeScript ```typescript interface ListNode { val: number; next: ListNode | null; } function reverseList(head: ListNode | null): ListNode | null { let prev: ListNode | null = null; let curr = head; while (curr) { const next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; } ``` ## Python ```python def reverse_list(head): prev = None curr = head while curr: nxt = curr.next curr.next = prev prev = curr curr = nxt return prev ``` ## PHP ```php function reverseList(?Node $head): ?Node { $prev = null; $curr = $head; while ($curr !== null) { $next = $curr->next; $curr->next = $prev; $prev = $curr; $curr = $next; } return $prev; } ``` ## Java ```java public static ListNode reverseList(ListNode head) { ListNode prev = null; ListNode curr = head; while (curr != null) { ListNode next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; } public static class ListNode { public int val; public ListNode next; } ``` ## C# ```csharp public static ListNode ReverseList(ListNode head) { ListNode prev = null; ListNode curr = head; while (curr != null) { ListNode next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; } public class ListNode { public int val; public ListNode next; } ``` --- title: "Fast & slow pointers" url: https://algopath.pro/patterns/fast-slow-pointers language: en summary: "One pointer moves a single step at a time while the other moves two steps. The gap between them exposes cycles and midpoints." updated: 2026-10-17 --- # Fast & slow pointers One pointer moves a single step at a time while the other moves two steps. The gap between them exposes cycles and midpoints. ## How does Fast & slow pointers work? Start both pointers at the head. One moves a single step per round, the other moves two. Step them together inside one loop. Stop when the fast one runs off the end. If the list ends, there is no cycle. A null next is the proof. If a cycle exists, the fast pointer laps the slow one. They land on the same node. Inside the cycle the gap shrinks by one each round. So a meeting is guaranteed. When the fast pointer reaches the end, the slow one sits at the middle. That gives the midpoint free. Sources: [Wikipedia: Cycle detection](https://en.wikipedia.org/wiki/Cycle_detection) - `slow = 1, fast = 1` The list is 1 to 5. Both pointers start at the head. - `slow = 2, fast = 3` One step against two. The gap is one node. - `slow = 3, fast = 5` The gap is now two. The fast pointer is nearly out. - `fast.next is null` The list ended, so there is no cycle. - `slow = 3` The slow pointer sits on the middle node. Five nodes, middle is third. ## When should you use Fast & slow pointers? Use Fast & slow pointers when the problem statement says “detect a cycle in a linked list”, “find where a cycle begins” or “find the middle node of a list”. A solution built on it usually runs in O(n). - detect a cycle in a linked list - find where a cycle begins - find the middle node of a list - determine if a list is a palindrome - no extra memory / O(1) space, only pointers ## What is Fast & slow pointers confused with? Fast & slow pointers is most often confused with Two pointers (same direction), Hash set / map, Linked list reversal and Graph BFS / DFS. - **Two pointers (same direction)** - There one pointer reads and the other writes. Here both read, at different speeds. - **Hash set / map** - A set of visited nodes also catches a cycle. It costs O(n) memory instead of O(1). - **Linked list reversal** - Reversal rewires the links themselves. This pattern never changes a pointer in the list. - **Graph BFS / DFS** - A traversal finds cycles in any graph, with a visited set. This works because each node has one exit. ## What is the time and space complexity of Fast & slow pointers? The target complexity of Fast & slow pointers is O(n). n up to 1e6 gives O(n) time and O(1) space. The slow pointer never walks more than n steps. ## A worked example of Fast & slow pointers ### Where the cycle starts A linked list may loop back into itself somewhere. Return the node where that loop begins. If the list has no loop, return null. First let the slow and fast pointers meet inside the loop. Then move one pointer back to the head. Step both one at a time, and they meet at the entry. ```javascript function detectCycle(head) { let slow = head; let fast = head; while (fast && fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) { // head to entry is the same distance as meeting point to entry let walker = head; while (walker !== slow) { walker = walker.next; slow = slow.next; } return walker; } } return null; } ``` ## Common mistakes with Fast & slow pointers - **Checking only fast in the loop condition** Reading fast.next.next throws when fast.next is null. Test both before stepping. - **Starting the two pointers apart** A head start changes which node they meet on. The entry proof assumes the same start. - **Comparing values instead of nodes** Two different nodes can hold the same value. Compare the references themselves. - **Treating the meeting point as the entry** The two pointers meet somewhere inside the loop, not at its start. A second walk finds the entry. ## Which interview problems use Fast & slow pointers? Interview problems solved with Fast & slow pointers include Linked list cycle, Linked list cycle II, Middle of the linked list, Happy number, Find the duplicate number, Palindrome linked list and Remove nth node from the end. - **Linked list cycle** The meeting itself is the whole answer. - **Linked list cycle II** A second walk from the head finds where it enters. - **Middle of the linked list** When fast ends, slow is standing on the middle. - **Happy number** The digit-square step builds an invisible linked list. - **Find the duplicate number** The array values act as next pointers. - **Palindrome linked list** Find the middle, reverse the back half, compare. - **Remove nth node from the end** A fixed gap instead of a doubled speed. ## JavaScript ```javascript function hasCycle(head) { let slow = head; let fast = head; while (fast && fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) return true; } return false; } ``` ## TypeScript ```typescript interface ListNode { val: number; next: ListNode | null; } function hasCycle(head: ListNode | null): boolean { let slow = head; let fast = head; while (fast && fast.next) { slow = slow!.next; fast = fast.next.next; if (slow === fast) return true; } return false; } ``` ## Python ```python def has_cycle(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: return True return False ``` ## PHP ```php function hasCycle(?Node $head): bool { $slow = $head; $fast = $head; while ($fast !== null && $fast->next !== null) { $slow = $slow->next; $fast = $fast->next->next; if ($slow === $fast) return true; } return false; } ``` ## Java ```java public static boolean hasCycle(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) return true; } return false; } public static class ListNode { public int val; public ListNode next; } ``` ## C# ```csharp public static bool HasCycle(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) return true; } return false; } public class ListNode { public int val; public ListNode next; } ``` --- title: "Linked list merge & reorder" url: https://algopath.pro/patterns/linked-list-merge language: en summary: "Build the answer on a dummy node and take from the front of each list. That dummy removes every empty-list special case." updated: 2026-10-17 --- # Linked list merge & reorder Build the answer on a dummy node and take from the front of each list. That dummy removes every empty-list special case. ## How does Linked list merge & reorder work? Create a dummy node that leads nowhere yet. Its next will end up being the real head. Keep a tail pointer starting at the dummy. Every node is appended there. Compare the front node of each list. Append the smaller one and step that list forward. Move the tail to the node just appended. Repeat until one of the lists runs out. Append whatever remains of the other list. It is already sorted, so no comparison is needed. Return dummy.next at the end. No node was copied, only re-linked. Sources: [Wikipedia: Merge algorithm](https://en.wikipedia.org/wiki/Merge_algorithm) - `dummy, a = 1, b = 2` Merging [1, 4] with [2, 3]. The dummy holds nothing. - `tail = 1, a = 4, b = 2` 1 is smaller than 2, so it is appended first. - `tail = 2, b = 3` Now 4 against 2. The 2 goes next. - `tail = 3, b = null` 3 beats 4 as well. The second list is now empty. - `1, 2, 3, 4` The rest of the first list is appended in one step. ## When should you use Linked list merge & reorder? Use Linked list merge & reorder when the problem statement says “merge two sorted linked lists”, “merge k sorted lists” or “reorder a list (interleave front and back halves)”. A solution built on it usually runs in O(n + m). - merge two sorted linked lists - merge k sorted lists - reorder a list (interleave front and back halves) - rearrange nodes without copying values into an array - dummy head / sentinel node ## What is Linked list merge & reorder confused with? Linked list merge & reorder is most often confused with Linked list reversal, Binary heap / priority queue, Fast sort (merge / quick) and Fast & slow pointers. - **Linked list reversal** - Reversal flips the links inside one list. Merging re-links nodes across two lists. - **Binary heap / priority queue** - With k lists a heap picks the smallest front in log k. With two lists one comparison is enough. - **Fast sort (merge / quick)** - The merge step of merge sort is this exact loop. Here the lists arrive already sorted. - **Fast & slow pointers** - Reordering starts by finding the middle, which is that pattern. The weave afterwards is this one. ## What is the time and space complexity of Linked list merge & reorder? The target complexity of Linked list merge & reorder is O(n + m). n plus m nodes gives O(n + m) time and O(1) space. Merging k lists with a heap costs O(N log k). ## A worked example of Linked list merge & reorder ### Reorder a list from both ends Reorder a list as first, last, second, second last, and onward. The nodes have to be relinked. Copying the values into an array is not allowed. Find the middle with a slow and a fast pointer, then cut the list there. Reverse the back half. Weave the two halves together one node at a time. ```javascript function reorderList(head) { if (!head || !head.next) return head; let slow = head; let fast = head; while (fast.next && fast.next.next) { slow = slow.next; fast = fast.next.next; } let second = slow.next; slow.next = null; // cut the list in two, or the weave loops forever let prev = null; while (second) { const next = second.next; second.next = prev; prev = second; second = next; } let first = head; while (prev) { const a = first.next; const b = prev.next; first.next = prev; prev.next = a; first = a; prev = b; } return head; } ``` ## Common mistakes with Linked list merge & reorder - **Working without a dummy node** The first append then needs a special case for the empty result. A dummy deletes that branch. - **Dropping the leftover tail** When one list empties, the other still holds nodes. Link the whole rest in one step. - **Building new nodes** These problems expect the original nodes relinked. Copying doubles the memory for nothing. - **Leaving a cycle behind** Cutting a list means setting some next to null. Skipping that makes the list loop. ## Which interview problems use Linked list merge & reorder? Interview problems solved with Linked list merge & reorder include Merge two sorted lists, Merge k sorted lists, Sort list, Reorder list, Add two numbers, Partition list and Intersection of two linked lists. - **Merge two sorted lists** The plain form: a dummy and one comparison per node. - **Merge k sorted lists** A heap holds the front node of every list. - **Sort list** Split at the middle, sort both halves, then merge. - **Reorder list** Split, reverse the back half, then weave the two. - **Add two numbers** Walk both lists together and carry the overflow. - **Partition list** Two dummies, one for small values and one for the rest. - **Intersection of two linked lists** Two walkers that swap lists at the end. ## JavaScript ```javascript function mergeTwoLists(a, b) { const dummy = { next: null }; let tail = dummy; while (a && b) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a || b; return dummy.next; } ``` ## TypeScript ```typescript interface ListNode { val: number; next: ListNode | null; } function mergeTwoLists(a: ListNode | null, b: ListNode | null): ListNode | null { const dummy: ListNode = { val: 0, next: null }; let tail = dummy; while (a && b) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a ?? b; return dummy.next; } ``` ## Python ```python def merge_two_lists(a, b): dummy = Node(0) tail = dummy while a and b: if a.val <= b.val: tail.next, a = a, a.next else: tail.next, b = b, b.next tail = tail.next tail.next = a or b return dummy.next ``` ## PHP ```php function mergeTwoLists(?Node $a, ?Node $b): ?Node { $dummy = new Node(0); $tail = $dummy; while ($a !== null && $b !== null) { if ($a->val <= $b->val) { $tail->next = $a; $a = $a->next; } else { $tail->next = $b; $b = $b->next; } $tail = $tail->next; } $tail->next = $a ?? $b; return $dummy->next; } ``` ## Java ```java public static ListNode mergeTwoLists(ListNode a, ListNode b) { ListNode dummy = new ListNode(); ListNode tail = dummy; while (a != null && b != null) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a != null ? a : b; return dummy.next; } public static class ListNode { public int val; public ListNode next; } ``` ## C# ```csharp public static ListNode MergeTwoLists(ListNode a, ListNode b) { var dummy = new ListNode(); var tail = dummy; while (a != null && b != null) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a ?? b; return dummy.next; } public class ListNode { public int val; public ListNode next; } ``` --- title: "Stack (LIFO)" url: https://algopath.pro/patterns/stack language: en summary: "A stack always hands back the most recent item first. That makes it the tool for anything that has to close in reverse order." updated: 2026-10-17 --- # Stack (LIFO) A stack always hands back the most recent item first. That makes it the tool for anything that has to close in reverse order. ## How does Stack (LIFO) work? A stack has two moves: push and pop. The last thing pushed is the first thing back. An array holds one perfectly well. Push writes at the end, pop reads and shrinks. Push whenever something opens. That may be a bracket, a directory or a pending calculation. Pop whenever something closes. Then check that the popped item matches the thing closing it. An empty stack at a pop means the input is broken. A non-empty stack at the end means something never closed. Nothing below the top is ever read. That restriction is what keeps each operation O(1). Sources: [Wikipedia: Stack (abstract data type)](https://en.wikipedia.org/wiki/Stack_(abstract_data_type)) - `stack = []` Checking the string ([]). Nothing is open yet. - `stack = ['(']` The opening round bracket is pushed. - `stack = ['(', '[']` A square bracket is pushed on top of it. - `stack = ['(']` The closing square bracket pops its match. - `stack = []` The closing round bracket pops the last one. Empty means valid. ## When should you use Stack (LIFO)? Use Stack (LIFO) when the problem statement says “valid parentheses / balanced brackets”, “match the most recent open with the next close” or “undo, backtrack to the last state, nested structure”. A solution built on it usually runs in O(n). - valid parentheses / balanced brackets - match the most recent open with the next close - undo, backtrack to the last state, nested structure - evaluate an expression with nested operators - process in reverse order of arrival ## What is Stack (LIFO) confused with? Stack (LIFO) is most often confused with Monotonic stack, Recursion, Graph BFS / DFS and Monotonic deque (sliding window max/min). - **Monotonic stack** - That keeps the stack ordered so every pop answers a question. A plain stack only remembers what is open. - **Recursion** - Every recursive call already sits on a stack. Writing your own avoids the depth limit. - **Graph BFS / DFS** - A queue gives breadth-first order, a stack gives depth-first. Same traversal, different container. - **Monotonic deque (sliding window max/min)** - A deque can be read and trimmed at both ends. A stack only ever touches the top. ## What is the time and space complexity of Stack (LIFO)? The target complexity of Stack (LIFO) is O(n). Every push and pop is O(1), so n operations cost O(n). Memory is O(n) in the worst case. ## A worked example of Stack (LIFO) ### Simplify a file path You get an absolute path that may contain dots and double dots. Return the shortest path that means the same thing. A double dot goes up one directory, a single dot stays put. Split the path on slashes and walk the parts in order. Push a real name and pop on a double dot. Skip empty parts and single dots, then join the stack. ```javascript function simplifyPath(path) { const stack = []; for (const part of path.split("/")) { if (part === "" || part === ".") continue; if (part === "..") { stack.pop(); // popping an empty stack is a no-op at the root } else { stack.push(part); } } return "/" + stack.join("/"); } ``` ## Common mistakes with Stack (LIFO) - **Popping without checking for empty** A pop on an empty stack quietly returns undefined. Test the size before you trust the value. - **Ignoring what is left at the end** A stack that still holds items means something never closed. Check it after the loop finishes. - **Using shift instead of pop** shift takes from the front, which makes it a queue. That reverses the meaning of the whole loop. - **Pushing values when you need positions** Many of these problems ask how far back something was. Push the index and read the value from it. ## Which interview problems use Stack (LIFO)? Interview problems solved with Stack (LIFO) include Valid parentheses, Min stack, Evaluate reverse polish notation, Simplify path, Basic calculator, Decode string and Backspace string compare. - **Valid parentheses** Push each opening bracket, pop and match on each closing one. - **Min stack** A second stack of running minimums answers in O(1). - **Evaluate reverse polish notation** Push numbers, pop two on every operator. - **Simplify path** Push a directory, pop on a double dot. - **Basic calculator** Push the running result before an opening bracket. - **Decode string** Push the repeat count and the text before each bracket. - **Backspace string compare** Each backspace pops the last character kept. ## JavaScript ```javascript function isValid(s) { const stack = []; const pairs = { ')': '(', ']': '[', '}': '{' }; for (const ch of s) { if (ch in pairs) { if (stack.pop() !== pairs[ch]) return false; } else { stack.push(ch); } } return stack.length === 0; } ``` ## TypeScript ```typescript function isValid(s: string): boolean { const stack: string[] = []; const pairs: Record = { ')': '(', ']': '[', '}': '{' }; for (const ch of s) { if (ch in pairs) { if (stack.pop() !== pairs[ch]) return false; } else { stack.push(ch); } } return stack.length === 0; } ``` ## Python ```python def is_valid(s): stack = [] pairs = {')': '(', ']': '[', '}': '{'} for ch in s: if ch in pairs: if not stack or stack.pop() != pairs[ch]: return False else: stack.append(ch) return not stack ``` ## PHP ```php function isValid(string $s): bool { $stack = []; $pairs = [')' => '(', ']' => '[', '}' => '{']; foreach (str_split($s) as $ch) { if (isset($pairs[$ch])) { if (array_pop($stack) !== $pairs[$ch]) return false; } else { $stack[] = $ch; } } return count($stack) === 0; } ``` ## Java ```java public static boolean isValid(String s) { Deque stack = new ArrayDeque<>(); Map pairs = Map.of(')', '(', ']', '[', '}', '{'); for (char ch : s.toCharArray()) { if (pairs.containsKey(ch)) { if (stack.isEmpty() || stack.pop() != pairs.get(ch)) return false; } else { stack.push(ch); } } return stack.isEmpty(); } ``` ## C# ```csharp public static bool IsValid(string s) { var stack = new Stack(); var pairs = new Dictionary { [')'] = '(', [']'] = '[', ['}'] = '{', }; foreach (char ch in s) { if (pairs.ContainsKey(ch)) { if (stack.Count == 0 || stack.Pop() != pairs[ch]) return false; } else { stack.Push(ch); } } return stack.Count == 0; } ``` --- title: "Monotonic stack" url: https://algopath.pro/patterns/monotonic-stack language: en summary: "Keep the stack's values decreasing from bottom to top. Pop everything the new value beats, and each pop just found its answer." updated: 2026-10-22 --- # Monotonic stack Keep the stack's values decreasing from bottom to top. Pop everything the new value beats, and each pop just found its answer. ## How does Monotonic stack work? The stack holds indices, not values. Each index waits there for its answer. Monotonic means the values at the stacked indices only move one way. For next greater, they never rise from bottom to top. Before pushing a new index, pop every index the new value beats. Each pop is an answer. The comparison decides what you get. Pop while the top is smaller for next greater. Flip the comparison to pop while the top is bigger. That gives next smaller. For the previous greater element, pop while the top's value is not bigger. The index left on top is the answer, and an empty stack means none. Whatever survives on the stack never found an answer. Those slots keep the default. The inner loop looks quadratic. Each index is pushed once and popped once, so the total is linear. Sources: [Wikipedia: All nearest smaller values](https://en.wikipedia.org/wiki/All_nearest_smaller_values) - `[0]` Push index 0. Value 2 sits on the stack. - `[0, 1]` Value 1 loses to 2. Nothing pops, push index 1. - `[2]` Value 5 beats 1, then 2. Both pop, both answer 5. - `[2, 3]` Value 3 loses to 5. Push index 3. - `[2, 3]` Input ends. Indices 2 and 3 keep -1. ## When should you use Monotonic stack? Use Monotonic stack when the problem statement says “next greater / next smaller element”, “daily temperatures until it gets warmer” or “largest rectangle in a histogram”. A solution built on it usually runs in O(n). - next greater / next smaller element - daily temperatures until it gets warmer - largest rectangle in a histogram - trap rainwater between bars - span/streak that ends at the first bigger or smaller value ## What is Monotonic stack confused with? Monotonic stack is most often confused with Stack, Monotonic deque, Sliding window (variable), Binary heap and Sorting with a comparator. - **Stack** - A plain stack pops when you say so. A monotonic stack pops on a comparison. - **Monotonic deque** - A deque also drops elements that fall out of a window. With no expiry by age, a stack is enough. - **Sliding window (variable)** - A window answers questions about a range. This stack answers about one boundary element per index. - **Binary heap** - A heap gives the global maximum in O(log n). The stack gives the nearest bigger neighbour in O(1). - **Sorting with a comparator** - Sorting throws away positions. Next greater questions are entirely about position. ## What is the time and space complexity of Monotonic stack? The target complexity of Monotonic stack is O(n). n up to 1e5 elements gives O(n). Each element is pushed once and popped at most once. ## A worked example of Monotonic stack ### Daily temperatures You get a list of daily temperatures. For each day, count the days until a warmer one. No warmer day means 0. Nested loops run in O(n squared) and time out at 100000 days. Daily temperatures is next greater element, with a distance instead of a value. Walk left to right. Pop every day on the stack that is colder than today. Today is that day's first warmer one. The wait is the gap between the indices. Days left on the stack never warmed up. They keep their 0. ```javascript function dailyTemperatures(temps) { const wait = new Array(temps.length).fill(0); const stack = []; // indices, coldest at the bottom for (let today = 0; today < temps.length; today++) { while ( stack.length && temps[stack[stack.length - 1]] < temps[today] ) { const colder = stack.pop(); wait[colder] = today - colder; } stack.push(today); } return wait; // days still on the stack keep their 0 } ``` ## Common mistakes with Monotonic stack - **Pushing values instead of indices** You need the position to compute a distance. Push the index, read the value with temps[i]. - **Getting the comparison backwards** Smaller on top gives next greater. Bigger on top gives next smaller. - **Forgetting the leftovers** Elements still on the stack found no answer. Decide their value up front: -1, 0, or the length. - **Mishandling equal values** Strict < keeps duplicates on the stack, <= pops them. Test [2, 2, 2] before you trust it. ## Which interview problems use Monotonic stack? Interview problems solved with Monotonic stack include Next greater element, Daily temperatures, Largest rectangle in a histogram, Trapping rain water, Stock span, Remove k digits and Sum of subarray minimums. - **Next greater element** The pattern with nothing hidden. Everything else is a rewording. - **Daily temperatures** Next greater, but it asks for the distance. - **Largest rectangle in a histogram** Each bar needs its first shorter neighbour on both sides. - **Trapping rain water** Water sits between a bar and the next taller one. - **Stock span** Next greater run backwards, counting days instead of values. - **Remove k digits** Pop bigger digits to leave the smallest number. - **Sum of subarray minimums** Each value owns a span bounded by smaller neighbours. ## JavaScript ```javascript function nextGreater(nums) { const result = new Array(nums.length).fill(-1); const stack = []; for (let i = 0; i < nums.length; i++) { while (stack.length && nums[stack[stack.length - 1]] < nums[i]) { result[stack.pop()] = nums[i]; } stack.push(i); } return result; } ``` ## TypeScript ```typescript function nextGreater(nums: number[]): number[] { const result: number[] = new Array(nums.length).fill(-1); const stack: number[] = []; for (let i = 0; i < nums.length; i++) { while (stack.length && nums[stack[stack.length - 1]] < nums[i]) { result[stack.pop() as number] = nums[i]; } stack.push(i); } return result; } ``` ## Python ```python def next_greater(nums): result = [-1] * len(nums) stack = [] for i, x in enumerate(nums): while stack and nums[stack[-1]] < x: result[stack.pop()] = x stack.append(i) return result ``` ## PHP ```php function nextGreater(array $nums): array { $result = array_fill(0, count($nums), -1); $stack = []; foreach ($nums as $i => $x) { while ($stack && $nums[end($stack)] < $x) { $result[array_pop($stack)] = $x; } $stack[] = $i; } return $result; } ``` ## Java ```java public static int[] nextGreater(int[] nums) { int[] result = new int[nums.length]; Arrays.fill(result, -1); Deque stack = new ArrayDeque<>(); for (int i = 0; i < nums.length; i++) { while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) { result[stack.pop()] = nums[i]; } stack.push(i); } return result; } ``` ## C# ```csharp public static int[] NextGreater(int[] nums) { var result = new int[nums.Length]; Array.Fill(result, -1); var stack = new Stack(); for (int i = 0; i < nums.Length; i++) { while (stack.Count > 0 && nums[stack.Peek()] < nums[i]) { result[stack.Pop()] = nums[i]; } stack.Push(i); } return result; } ``` --- title: "Monotonic deque (sliding window max/min)" url: https://algopath.pro/patterns/monotonic-deque language: en summary: "A deque kept in order, so its front is always the window's maximum. Beaten values leave the back, expired ones the front." updated: 2026-10-17 --- # Monotonic deque (sliding window max/min) A deque kept in order, so its front is always the window's maximum. Beaten values leave the back, expired ones the front. ## How does Monotonic deque (sliding window max/min) work? The deque holds indices, never values. An index tells you when that element leaves the window. Before pushing a new index, drop every index at the back whose value is smaller. None of them can be the maximum again. Push the new index at the back. The values now fall from the front to the back. Look at the front index. If it has slid out of the window, remove it. The front now holds the window's maximum. Read it without scanning anything. Each index is pushed once and popped once. That is what keeps the whole pass linear. Sources: [Wikipedia: Double-ended queue](https://en.wikipedia.org/wiki/Double-ended_queue), [Python docs: collections.deque](https://docs.python.org/3/library/collections.html#collections.deque) - `deque = [0]` A window of 3 over [1, 3, -1, -3, 5]. Index 0 goes in. - `deque = [1]` 3 beats the 1 behind it. Index 0 is dropped from the back. - `deque = [1, 2]` The -1 is smaller, so it stays. The window is full and the max is 3. - `deque = [1, 2, 3]` The -3 is smaller again. The front index is still in range. - `deque = [4]` 5 beats everything and empties the deque. The max is 5. ## When should you use Monotonic deque (sliding window max/min)? Use Monotonic deque (sliding window max/min) when the problem statement says “maximum or minimum of every window of size k”, “sliding window maximum/minimum” or “shortest subarray with a sum at least K”. A solution built on it usually runs in O(n). - maximum or minimum of every window of size k - sliding window maximum/minimum - shortest subarray with a sum at least K - need both ends of a queue popped/pushed - deque, double-ended queue ## What is Monotonic deque (sliding window max/min) confused with? Monotonic deque (sliding window max/min) is most often confused with Monotonic stack, Sliding window (fixed size), Binary heap / priority queue and Stack (LIFO). - **Monotonic stack** - A stack only ever drops from the top. A deque also drops the front when a value leaves the window. - **Sliding window (fixed size)** - A rolling sum survives a subtraction. A maximum does not, which is why this exists. - **Binary heap / priority queue** - A heap gives the maximum in log n. It cannot delete the element that just left the window. - **Stack (LIFO)** - A plain stack keeps no order and has one open end. Both limits break this problem. ## What is the time and space complexity of Monotonic deque (sliding window max/min)? The target complexity of Monotonic deque (sliding window max/min) is O(n). n up to 1e6 gives O(n), since each index enters and leaves once. Memory is O(k) for the window. ## A worked example of Monotonic deque (sliding window max/min) ### Longest stretch within a limit Find the longest stretch whose largest and smallest values differ by at most a limit. The stretch has to be contiguous. Grow a variable window and keep two deques, one for maxima and one for minima. When the two fronts differ by more than the limit, shrink from the left. Drop a front index once it expires. ```javascript function longestSubarray(nums, limit) { const maxQ = []; // indices, values falling from front to back const minQ = []; // indices, values rising from front to back let left = 0; let best = 0; for (let right = 0; right < nums.length; right++) { while (maxQ.length && nums[maxQ[maxQ.length - 1]] <= nums[right]) maxQ.pop(); while (minQ.length && nums[minQ[minQ.length - 1]] >= nums[right]) minQ.pop(); maxQ.push(right); minQ.push(right); while (nums[maxQ[0]] - nums[minQ[0]] > limit) { if (maxQ[0] === left) maxQ.shift(); if (minQ[0] === left) minQ.shift(); left++; } best = Math.max(best, right - left + 1); } return best; } ``` ## Common mistakes with Monotonic deque (sliding window max/min) - **Storing values instead of indices** Without an index you cannot tell when an element expires. Push the position and read the value from it. - **Trimming the front before pushing** Order matters here. Drop the beaten values at the back first, then check the front. - **Being careless about equal values** Whether you pop on equality decides which duplicate survives. It matters when the answer is an index. - **Reaching for a heap instead** A heap cannot remove the element that just left the window. Its deletions are lazy and cost extra. ## Which interview problems use Monotonic deque (sliding window max/min)? Interview problems solved with Monotonic deque (sliding window max/min) include Sliding window maximum, Longest subarray with absolute diff within a limit, Shortest subarray with sum at least k, Jump game VI, Constrained subsequence sum, Max value of equation and Sliding window minimum. - **Sliding window maximum** The plain form: read the front at every step. - **Longest subarray with absolute diff within a limit** Two deques, one for each end. - **Shortest subarray with sum at least k** Prefix sums with a rising deque over them. - **Jump game VI** The best score inside the window you can reach. - **Constrained subsequence sum** The same window, run over a DP array. - **Max value of equation** A window over x, keyed by y minus x. - **Sliding window minimum** The same code with the comparison flipped. ## JavaScript ```javascript function maxSlidingWindow(nums, k) { const deque = []; // stores indices, values decreasing const result = []; for (let i = 0; i < nums.length; i++) { while (deque.length && deque[0] <= i - k) deque.shift(); while (deque.length && nums[deque[deque.length - 1]] < nums[i]) deque.pop(); deque.push(i); if (i >= k - 1) result.push(nums[deque[0]]); } return result; } ``` ## TypeScript ```typescript function maxSlidingWindow(nums: number[], k: number): number[] { const deque: number[] = []; // stores indices, values decreasing const result: number[] = []; for (let i = 0; i < nums.length; i++) { while (deque.length && deque[0] <= i - k) deque.shift(); while (deque.length && nums[deque[deque.length - 1]] < nums[i]) deque.pop(); deque.push(i); if (i >= k - 1) result.push(nums[deque[0]]); } return result; } ``` ## Python ```python from collections import deque as dq def max_sliding_window(nums, k): d = dq() # indices, values decreasing result = [] for i, x in enumerate(nums): while d and d[0] <= i - k: d.popleft() while d and nums[d[-1]] < x: d.pop() d.append(i) if i >= k - 1: result.append(nums[d[0]]) return result ``` ## PHP ```php function maxSlidingWindow(array $nums, int $k): array { $deque = []; // indices, values decreasing $result = []; foreach ($nums as $i => $x) { while ($deque && $deque[0] <= $i - $k) array_shift($deque); while ($deque && $nums[end($deque)] < $x) array_pop($deque); $deque[] = $i; if ($i >= $k - 1) $result[] = $nums[$deque[0]]; } return $result; } ``` ## Java ```java public static int[] maxSlidingWindow(int[] nums, int k) { Deque deque = new ArrayDeque<>(); // indices, values decreasing int[] result = new int[nums.length - k + 1]; int out = 0; for (int i = 0; i < nums.length; i++) { while (!deque.isEmpty() && deque.peekFirst() <= i - k) deque.pollFirst(); while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) deque.pollLast(); deque.addLast(i); if (i >= k - 1) result[out++] = nums[deque.peekFirst()]; } return result; } ``` ## C# ```csharp public static int[] MaxSlidingWindow(int[] nums, int k) { var deque = new LinkedList(); // indices, values decreasing var result = new int[nums.Length - k + 1]; int outIndex = 0; for (int i = 0; i < nums.Length; i++) { while (deque.Count > 0 && deque.First.Value <= i - k) deque.RemoveFirst(); while (deque.Count > 0 && nums[deque.Last.Value] < nums[i]) deque.RemoveLast(); deque.AddLast(i); if (i >= k - 1) result[outIndex++] = nums[deque.First.Value]; } return result; } ``` --- title: "Recursion" url: https://algopath.pro/patterns/recursion language: en summary: "A function that calls itself on a smaller version of the same problem. A base case stops it, and the calls unwind back up." updated: 2026-10-17 --- # Recursion A function that calls itself on a smaller version of the same problem. A base case stops it, and the calls unwind back up. ## How does Recursion work? Find the smallest case you can answer with no work at all. That is the base case. Assume the function already works on a smaller input. You trust it rather than tracing it. Write one step that makes the problem smaller. Then call yourself on what is left. Combine the returned result with that one step. Those two lines are the whole body. Every call takes a frame of memory. Depth n means n frames are alive at once. Without a base case the calls never stop. The stack fills and the program dies. Sources: [Wikipedia: Recursion (computer science)](https://en.wikipedia.org/wiki/Recursion_(computer_science)) - `sum([2, 4, 6])` The list is not empty. Take the 2 and ask for the rest. - `sum([4, 6])` Take the 4 and ask again. The list keeps shrinking. - `sum([6])` One element is left. This is still not the base case. - `sum([]) = 0` The empty list is the base case. It answers with no work. - `0, 6, 10, 12` The results unwind back up. Each call adds its own value. ## When should you use Recursion? Use Recursion when the problem statement says “problem is defined in terms of a smaller version of itself”, “nested/tree-shaped structure to walk (directory, expression, linked list)” or “compute a count/sum/depth over all sub-elements”. A solution built on it usually runs in O(n). - problem is defined in terms of a smaller version of itself - nested/tree-shaped structure to walk (directory, expression, linked list) - compute a count/sum/depth over all sub-elements - natural base case + recursive case split (factorial, Fibonacci, tree depth) ## What is Recursion confused with? Recursion is most often confused with Recursion with memoization, Backtracking, Stack (LIFO) and Tree traversal. - **Recursion with memoization** - Memoisation is this plus a cache of past answers. Add it once the same argument comes back. - **Backtracking** - Backtracking undoes a choice after exploring it. Plain recursion never undoes anything. - **Stack (LIFO)** - The call stack is a stack you did not write. Writing your own removes the depth limit. - **Tree traversal** - Trees are where recursion is cheapest and clearest. That page is about the three visit orders. ## What is the time and space complexity of Recursion? The target complexity of Recursion is O(n). Depth up to roughly 10000 is safe in a browser. Deeper than that, rewrite the recursion as a loop. ## A worked example of Recursion ### Flatten a nested array You get an array whose items are either numbers or more arrays. Return one flat array of numbers. The nesting can go to any depth at all. A number is the base case, so it is returned as it is. An array is the smaller problem: flatten each item and join the pieces. The depth handles itself. ```javascript function flatten(items) { const result = []; for (const item of items) { if (Array.isArray(item)) { // trust the call to handle every depth below this one result.push(...flatten(item)); } else { result.push(item); } } return result; } ``` ## Common mistakes with Recursion - **No base case, or the wrong one** The calls then run until the stack fills up. Write the base case before anything else. - **Not shrinking the input** Every call has to move closer to the base case. Passing the same argument loops forever. - **Recursing far too deep** A list of a million nodes needs a million frames. Rewrite that as a loop. - **Sharing one mutable array between calls** A child call can change what the parent still needs. Copy it, or undo the change on the way out. ## Which interview problems use Recursion? Interview problems solved with Recursion include Factorial and Fibonacci, Reverse a string, Flatten a nested list, Merge two sorted lists, Power of a number, Tower of Hanoi and Maximum depth of a binary tree. - **Factorial and Fibonacci** The classic shape, though Fibonacci needs a cache. - **Reverse a string** Swap the two ends, then recurse on the middle. - **Flatten a nested list** A plain value is the base case. - **Merge two sorted lists** Take the smaller head and recurse on the rest. - **Power of a number** Halve the exponent instead of subtracting one. - **Tower of Hanoi** Two smaller moves wrapped around one direct move. - **Maximum depth of a binary tree** One plus the deeper of the two children. ## JavaScript ```javascript function recurse(n) { if (n <= 0) return 0; // base case return n + recurse(n - 1); // recursive case: smaller subproblem } ``` ## TypeScript ```typescript function recurse(n: number): number { if (n <= 0) return 0; // base case return n + recurse(n - 1); // recursive case: smaller subproblem } ``` ## Python ```python def recurse(n): if n <= 0: return 0 # base case return n + recurse(n - 1) # recursive case: smaller subproblem ``` ## PHP ```php function recurse(int $n): int { if ($n <= 0) return 0; // base case return $n + recurse($n - 1); // recursive case: smaller subproblem } ``` ## Java ```java public static int recurse(int n) { if (n <= 0) return 0; // base case return n + recurse(n - 1); // recursive case: smaller subproblem } ``` ## C# ```csharp public static int Recurse(int n) { if (n <= 0) return 0; // base case return n + Recurse(n - 1); // recursive case: smaller subproblem } ``` --- title: "Recursion with memoization" url: https://algopath.pro/patterns/memoization language: en summary: "Recursion that writes down every answer it computes. When the same argument comes back, the stored value is handed back." updated: 2026-10-17 --- # Recursion with memoization Recursion that writes down every answer it computes. When the same argument comes back, the stored value is handed back. ## How does Recursion with memoization work? Write the plain recursion first. Get it correct before trying to make it fast. Look at what the arguments really are. If two calls share them, they share the answer. Add a cache keyed by those arguments. A map or a plain array both work. At the top of the function, return the cached answer if one exists. Nothing else runs. Before returning, store the answer under its key. The next call reads it instead of recomputing. Each distinct argument is now computed once. The tree of calls collapses into a graph. Sources: [Wikipedia: Memoization](https://en.wikipedia.org/wiki/Memoization), [Python docs: functools.cache](https://docs.python.org/3/library/functools.html#functools.cache) - `fib(5) needs fib(4) and fib(3)` The naive version splits into two calls. - `fib(4) needs fib(3) and fib(2)` Now fib(3) is wanted twice over. - `cache = {2: 1, 3: 2}` The first fib(3) is computed and stored. - `fib(3) is a lookup` The second fib(3) reads the cache. Its whole subtree is skipped. - `15 calls become 9` Without a cache, fib(50) would take billions of calls. ## When should you use Recursion with memoization? Use Recursion with memoization when the problem statement says “recursive calls repeat the same arguments (overlapping subproblems)”, “plain recursion is correct but too slow / exponential blow-up” or “small number of distinct states even though the recursion tree is huge”. A solution built on it usually runs in O(states). - recursive calls repeat the same arguments (overlapping subproblems) - plain recursion is correct but too slow / exponential blow-up - small number of distinct states even though the recursion tree is huge - cache/memo the result of (i, ...) before returning - Fibonacci / climbing stairs / grid-path wording with a hint to cache ## What is Recursion with memoization confused with? Recursion with memoization is most often confused with Recursion, Dynamic programming (1-D), Hash set / map and Backtracking. - **Recursion** - Plain recursion recomputes a repeated argument every time. The cache is the entire difference. - **Dynamic programming (1-D)** - Bottom-up DP fills a table in a fixed order. This fills it on demand, from the top down. - **Hash set / map** - The cache is usually a map. This page is about deciding when to keep one. - **Backtracking** - Backtracking explores paths that all differ, so nothing repeats. A cache there stores keys never asked again. ## What is the time and space complexity of Recursion with memoization? The target complexity of Recursion with memoization is O(states). Cost is the number of distinct arguments times the work per call. A cache of 1e6 keys is fine. ## A worked example of Recursion with memoization ### Fewest coins for an amount You get a list of coin values and an amount. Return the fewest coins that add up to it. If no combination works, return minus one. Ask the same question about a smaller amount. Each coin gives one branch. The remaining amount alone identifies a subproblem. So the cache is keyed by that amount. ```javascript function coinChange(coins, amount) { const cache = new Map(); function best(left) { if (left === 0) return 0; if (left < 0) return Infinity; if (cache.has(left)) return cache.get(left); // computed before let answer = Infinity; for (const coin of coins) { answer = Math.min(answer, best(left - coin) + 1); } cache.set(left, answer); // store the failure too return answer; } const result = best(amount); return result === Infinity ? -1 : result; } ``` ## Common mistakes with Recursion with memoization - **A key that leaves out an argument** Every value the answer depends on belongs in the key. A partial key returns another call's answer. - **Caching a mutable object** Storing a reference lets a later call change it. Store a copy or a plain value. - **Not caching a failure** A branch that fails is still an answer worth keeping. Otherwise dead ends get explored again. - **Caching where nothing repeats** If every argument is unique, the cache only costs memory. Check the overlap before adding it. ## Which interview problems use Recursion with memoization? Interview problems solved with Recursion with memoization include Fibonacci, Climbing stairs, Coin change, House robber, Word break, Unique paths and Longest increasing path in a matrix. - **Fibonacci** The shortest example of an overlapping subproblem. - **Climbing stairs** The same shape with a different base case. - **Coin change** Keyed by the amount still left. - **House robber** Keyed by the index you are standing on. - **Word break** Keyed by the start position in the string. - **Unique paths** Keyed by the pair of coordinates. - **Longest increasing path in a matrix** Keyed by the cell, and no visited set is needed. ## JavaScript ```javascript function fib(n, memo = new Map()) { if (n <= 1) return n; if (memo.has(n)) return memo.get(n); const result = fib(n - 1, memo) + fib(n - 2, memo); memo.set(n, result); return result; } ``` ## TypeScript ```typescript function fib(n: number, memo: Map = new Map()): number { if (n <= 1) return n; const cached = memo.get(n); if (cached !== undefined) return cached; const result = fib(n - 1, memo) + fib(n - 2, memo); memo.set(n, result); return result; } ``` ## Python ```python def fib(n, memo=None): if memo is None: memo = {} if n <= 1: return n if n in memo: return memo[n] memo[n] = fib(n - 1, memo) + fib(n - 2, memo) return memo[n] ``` ## PHP ```php function fib(int $n, array &$memo = []): int { if ($n <= 1) return $n; if (isset($memo[$n])) return $memo[$n]; return $memo[$n] = fib($n - 1, $memo) + fib($n - 2, $memo); } ``` ## Java ```java public static long fib(int n, Map memo) { if (n <= 1) return n; Long cached = memo.get(n); if (cached != null) return cached; long result = fib(n - 1, memo) + fib(n - 2, memo); memo.put(n, result); return result; } ``` ## C# ```csharp public static long Fib(int n, Dictionary memo) { if (n <= 1) return n; if (memo.TryGetValue(n, out long cached)) return cached; long result = Fib(n - 1, memo) + Fib(n - 2, memo); memo[n] = result; return result; } ``` --- title: "Backtracking" url: https://algopath.pro/patterns/backtracking language: en summary: "Make a choice, recurse, and then take the choice back. That undo is what lets a single array hold every candidate in turn." updated: 2026-10-17 --- # Backtracking Make a choice, recurse, and then take the choice back. That undo is what lets a single array hold every candidate in turn. ## How does Backtracking work? Keep one array holding the choices made so far. It is the path down the tree. At each level, loop over the options still available. Every option is one branch. Push the option, then call yourself for the next level. Nothing else changes. When that call returns, pop the option off again. The array is exactly as it was. At the bottom, record a copy of the path. Recording the array itself stores a shared buffer. Cut a branch as soon as it cannot work. That pruning is where the speed comes from. Sources: [Wikipedia: Backtracking](https://en.wikipedia.org/wiki/Backtracking) - `path = []` Listing the subsets of [1, 2]. The empty set already counts. - `path = [1]` Take the 1 and go one level down. - `path = [1, 2]` Take the 2 as well. This branch is finished. - `path = [1], then []` Two pops undo both choices. The array is empty again. - `path = [2]` Skip the 1 and take the 2. Four subsets in total. ## When should you use Backtracking? Use Backtracking when the problem statement says “generate all subsets/permutations/combinations”, “n is small (roughly n <= 12-20), exponential search is acceptable” or “make a choice, recurse, then undo it before trying the next choice”. A solution built on it usually runs in O(2^n)/O(n!). - generate all subsets/permutations/combinations - n is small (roughly n <= 12-20), exponential search is acceptable - make a choice, recurse, then undo it before trying the next choice - place items under constraints and back off on conflict (N-Queens, Sudoku) - return every valid arrangement, not just one ## What is Backtracking confused with? Backtracking is most often confused with Recursion, Recursion with memoization, Dynamic programming (1-D) and Matrix word search (grid DFS/backtracking). - **Recursion** - Recursion goes down and comes back. Backtracking also puts the state back as it returns. - **Recursion with memoization** - A cache pays off when arguments repeat. Down a path of distinct choices they rarely do. - **Dynamic programming (1-D)** - DP counts or optimises without listing anything. Backtracking is for when you need the arrangements. - **Matrix word search (grid DFS/backtracking)** - That is this pattern on a grid. The choice there is which neighbour to step into. ## What is the time and space complexity of Backtracking? The target complexity of Backtracking is O(2^n)/O(n!). The output is exponential, so n stays small. Subsets of 20 items are 1e6, permutations of 10 are 3.6e6. ## A worked example of Backtracking ### Every combination that hits a target You get distinct positive numbers and a target. List every combination that adds up to it. A number may be used as many times as you like. At each level, try every number from the current index onward. Starting at the current index stops the same combination appearing reordered. A negative remainder cuts the branch. ```javascript function combinationSum(candidates, target) { const result = []; const path = []; function walk(start, left) { if (left === 0) { result.push([...path]); // a copy, never the live array return; } if (left < 0) return; // pruned: this branch cannot recover for (let i = start; i < candidates.length; i++) { path.push(candidates[i]); walk(i, left - candidates[i]); // i, not i + 1: reuse is allowed path.pop(); // undo before the next option } } walk(0, target); return result; } ``` ## Common mistakes with Backtracking - **Storing the live path in the results** Every result then points at the same array. Push a copy of it instead. - **Forgetting to undo the choice** The path grows and never shrinks. Later branches then inherit choices that were not theirs. - **Starting the inner loop at zero** The same combination then appears in every possible order. Pass the current index down. - **Never pruning a branch** Without an early exit the entire tree gets explored. Most of these are only fast because of the cut. ## Which interview problems use Backtracking? Interview problems solved with Backtracking include Subsets, Permutations, Combination sum, N-queens, Word search, Palindrome partitioning and Sudoku solver. - **Subsets** Two options per element: take it or skip it. - **Permutations** Every unused element is an option at each level. - **Combination sum** The index passed down decides whether reuse is allowed. - **N-queens** Prune on the column and on both diagonals. - **Word search** The grid is the tree, and neighbours are the choices. - **Palindrome partitioning** Cut at every position whose prefix is a palindrome. - **Sudoku solver** Nine options per empty cell, pruned by the rules. ## JavaScript ```javascript function backtrack(path, choices, result) { if (path.length === choices.length) { // or another stop condition result.push([...path]); return; } for (const choice of choices) { path.push(choice); // choose backtrack(path, choices, result); // explore path.pop(); // undo } } ``` ## TypeScript ```typescript function backtrack(path: number[], choices: number[], result: number[][]): void { if (path.length === choices.length) { // or another stop condition result.push([...path]); return; } for (const choice of choices) { path.push(choice); // choose backtrack(path, choices, result); // explore path.pop(); // undo } } ``` ## Python ```python def backtrack(path, choices, result): if len(path) == len(choices): # or another stop condition result.append(path[:]) return for choice in choices: path.append(choice) # choose backtrack(path, choices, result) # explore path.pop() # undo ``` ## PHP ```php function backtrack(array &$path, array $choices, array &$result): void { if (count($path) === count($choices)) { $result[] = $path; return; } foreach ($choices as $choice) { $path[] = $choice; // choose backtrack($path, $choices, $result); // explore array_pop($path); // undo } } ``` ## Java ```java public static void backtrack(List path, int[] choices, List> result) { if (path.size() == choices.length) { // or another stop condition result.add(new ArrayList<>(path)); return; } for (int choice : choices) { path.add(choice); // choose backtrack(path, choices, result); // explore path.remove(path.size() - 1); // undo } } ``` ## C# ```csharp public static void Backtrack(List path, int[] choices, List> result) { if (path.Count == choices.Length) { // or another stop condition result.Add(new List(path)); return; } foreach (int choice in choices) { path.Add(choice); // choose Backtrack(path, choices, result); // explore path.RemoveAt(path.Count - 1); // undo } } ``` --- title: "Tree traversal" url: https://algopath.pro/patterns/tree-traversal language: en summary: "Visit every node of the tree exactly once. Where exactly you place the visit is what decides what the traversal computes." updated: 2026-10-17 --- # Tree traversal Visit every node of the tree exactly once. Where exactly you place the visit is what decides what the traversal computes. ## How does Tree traversal work? Every traversal does the same two things at a node. It visits the node and descends into each child. Pre-order visits the node before its children. Use it when the parent's value is needed first. In-order visits the left child, then the node, then the right. On a search tree that yields sorted order. Post-order visits both children before the node. Use it when the answer depends on the children. Level-order uses a queue instead of recursion. It works in rows rather than in depth. Only the position of the visit changes. The walking part is identical in all of them. Sources: [Wikipedia: Tree traversal](https://en.wikipedia.org/wiki/Tree_traversal) - `root 1, children 2 and 3` The root is reached first, whatever the order. - `pre-order: 1, 2, 3` The node is recorded, then both of its children. - `in-order: 2, 1, 3` The left child is recorded before the node itself. - `post-order: 2, 3, 1` Both children finish before the node is recorded. - `level-order: 1, 2, 3` A queue reads row by row instead of going deep. ## When should you use Tree traversal? Use Tree traversal when the problem statement says “binary tree given as a root node with left/right pointers”, “visit every node (preorder / inorder / postorder / level-order)” or “compute a depth, height, or sum over the whole tree”. A solution built on it usually runs in O(n). - binary tree given as a root node with left/right pointers - visit every node (preorder / inorder / postorder / level-order) - compute a depth, height, or sum over the whole tree - process children before or after the parent - level-by-level output (BFS with a queue) ## What is Tree traversal confused with? Tree traversal is most often confused with Graph BFS / DFS, Recursion, Stack (LIFO) and Binary search tree. - **Graph BFS / DFS** - A graph can reach a node twice, so it needs a visited set. A tree cannot, so it does not. - **Recursion** - Recursion is how a traversal is usually written. This page is about the three visit orders. - **Stack (LIFO)** - An iterative traversal swaps the call stack for your own. The visiting order stays the same. - **Binary search tree** - In a search tree, in-order gives sorted values. In any other tree that order means nothing. ## What is the time and space complexity of Tree traversal? The target complexity of Tree traversal is O(n). n nodes give O(n) time. Space is O(h) for the stack, and h equals n on a chain. ## A worked example of Tree traversal ### Diameter of a binary tree Return the longest path between any two nodes, counted in edges. The path does not have to pass through the root. At any node, the best local path is the left depth plus the right depth. So compute the depth in post-order. Record the best sum while the calls unwind. ```javascript function diameterOfBinaryTree(root) { let best = 0; function depth(node) { if (!node) return 0; const left = depth(node.left); const right = depth(node.right); // the path passing through this node, counted in edges best = Math.max(best, left + right); return 1 + Math.max(left, right); } depth(root); return best; } ``` ## Common mistakes with Tree traversal - **Missing the empty-node check** Every recursion has to stop at an empty child. That check is the base case. - **Putting the visit in the wrong place** The visit line is the only difference between the orders. Moving it changes the answer. - **Pushing children in the wrong order** An iterative pre-order pushes the right child first. A stack reverses whatever you give it. - **Recursing down a very deep tree** A million nodes in one chain overflows the call stack. Use an explicit stack there. ## Which interview problems use Tree traversal? Interview problems solved with Tree traversal include Binary tree inorder traversal, Maximum depth of a binary tree, Diameter of a binary tree, Binary tree level order traversal, Path sum, Invert a binary tree and Binary tree right side view. - **Binary tree inorder traversal** The plain form, recursive or with a stack. - **Maximum depth of a binary tree** One plus the deeper of the two children. - **Diameter of a binary tree** Left depth plus right depth at every node. - **Binary tree level order traversal** A queue, processing one row per round. - **Path sum** Carry the running total down the tree. - **Invert a binary tree** Swap the two children at every node. - **Binary tree right side view** The last node reached on each level. ## JavaScript ```javascript function traverse(node, result = []) { if (!node) return result; // base case traverse(node.left, result); result.push(node.val); // inorder here; move this line for pre/post traverse(node.right, result); return result; } ``` ## TypeScript ```typescript interface TreeNode { val: number; left: TreeNode | null; right: TreeNode | null; } function traverse(node: TreeNode | null, result: number[] = []): number[] { if (!node) return result; // base case traverse(node.left, result); result.push(node.val); // inorder here; move this line for pre/post traverse(node.right, result); return result; } ``` ## Python ```python def traverse(node, result=None): if result is None: result = [] if not node: return result traverse(node.left, result) result.append(node.val) # inorder here; move this line for pre/post traverse(node.right, result) return result ``` ## PHP ```php function traverse(?TreeNode $node, array &$result = []): array { if ($node === null) return $result; traverse($node->left, $result); $result[] = $node->val; // inorder here; move this line for pre/post traverse($node->right, $result); return $result; } ``` ## Java ```java public static List traverse(TreeNode node, List result) { if (node == null) return result; // base case traverse(node.left, result); result.add(node.val); // inorder here; move this line for pre/post traverse(node.right, result); return result; } public static class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` ## C# ```csharp public static List Traverse(TreeNode node, List result) { if (node == null) return result; // base case Traverse(node.left, result); result.Add(node.val); // inorder here; move this line for pre/post Traverse(node.right, result); return result; } public class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` --- title: "Binary search tree" url: https://algopath.pro/patterns/bst language: en summary: "A search tree keeps all the smaller values to the left and the larger ones to the right. Every lookup drops one whole side." updated: 2026-10-17 --- # Binary search tree A search tree keeps all the smaller values to the left and the larger ones to the right. Every lookup drops one whole side. ## How does Binary search tree work? Every node splits its own subtree in two. Everything left is smaller, everything right is larger. A search compares the target against the current node. That comparison picks one child and drops the other. An insert walks the same path down. The new node becomes a child of the last node reached. The rule covers whole subtrees, not just direct children. A left grandchild still has to be smaller than the root. An in-order traversal reads the values in sorted order. That is the property most problems lean on. Sorted input builds one long chain. Every operation then costs n, and balancing is the fix. Sources: [Wikipedia: Binary search tree](https://en.wikipedia.org/wiki/Binary_search_tree) - `root = 5` Inserting 5, 3, 8 and 4. The first value becomes the root. - `3 goes left` 3 is smaller than 5. It becomes the left child. - `8 goes right` 8 is larger than 5. It becomes the right child. - `4 goes left, then right` 4 is smaller than 5, then larger than 3. - `in-order: 3, 4, 5, 8` Reading in order gives the values sorted. ## When should you use Binary search tree? Use Binary search tree when the problem statement says “binary search tree: left < node < right at every node”, “insert/delete/search while keeping the data ordered” or “validate BST / kth smallest / inorder traversal gives sorted order”. A solution built on it usually runs in O(log n) avg. - binary search tree: left < node < right at every node - insert/delete/search while keeping the data ordered - validate BST / kth smallest / inorder traversal gives sorted order - closest value or range query on a set that changes over time - no array given up front, values arrive one at a time ## What is Binary search tree confused with? Binary search tree is most often confused with Binary search (array), Hash set / map, Binary heap / priority queue and Tree traversal. - **Binary search (array)** - An array halves by index but cannot take a cheap insert. A tree halves by pointer and can. - **Hash set / map** - A map is faster for an exact lookup. A tree also answers ranges and gives sorted order. - **Binary heap / priority queue** - A heap only promises the smallest value at the root. A search tree orders every pair of nodes. - **Tree traversal** - That page is about visiting a whole tree. This one is about the ordering rule. ## What is the time and space complexity of Binary search tree? The target complexity of Binary search tree is O(log n) avg. Balanced, n up to 1e6 gives O(log n) per operation. Unbalanced, the same tree degrades to O(n). ## A worked example of Binary search tree ### Check that a tree is a search tree Decide whether a binary tree obeys the search tree rule. Every value in the left subtree must be smaller. Every value on the right must be larger. Comparing a node against its two children is not enough. Carry a low and a high bound down the tree instead. Each step tightens one of the two. ```javascript function isValidBST(root, low = -Infinity, high = Infinity) { if (!root) return true; // the bound comes from an ancestor, not from the parent alone if (root.val <= low || root.val >= high) return false; return isValidBST(root.left, low, root.val) && isValidBST(root.right, root.val, high); } ``` ## Common mistakes with Binary search tree - **Checking only the direct children** A deep node can break the rule against a distant ancestor. Pass bounds down instead. - **Assuming the tree is balanced** Sorted inserts build a chain, not a tree. Then log n is really n. - **Leaving duplicates undecided** The problem has to say which side a repeated value goes. Pick a side and hold to it. - **Deleting a node with two children carelessly** A node with two children is replaced by its in-order successor. Any other node breaks the ordering. ## Which interview problems use Binary search tree? Interview problems solved with Binary search tree include Validate binary search tree, Search in a BST, Insert into a BST, Delete node in a BST, Kth smallest element in a BST, Lowest common ancestor of a BST and Convert sorted array to a BST. - **Validate binary search tree** Carry a low and a high bound down. - **Search in a BST** One comparison drops half the tree. - **Insert into a BST** Walk down and hang the node off the last one. - **Delete node in a BST** Replace it with its in-order successor. - **Kth smallest element in a BST** Stop the in-order walk after k nodes. - **Lowest common ancestor of a BST** The first node that sits between the two values. - **Convert sorted array to a BST** The middle element becomes the root, then recurse. ## JavaScript ```javascript function insert(node, val) { if (!node) return { val, left: null, right: null }; if (val < node.val) node.left = insert(node.left, val); else if (val > node.val) node.right = insert(node.right, val); return node; } ``` ## TypeScript ```typescript interface TreeNode { val: number; left: TreeNode | null; right: TreeNode | null; } function insert(node: TreeNode | null, val: number): TreeNode { if (!node) return { val, left: null, right: null }; if (val < node.val) node.left = insert(node.left, val); else if (val > node.val) node.right = insert(node.right, val); return node; } ``` ## Python ```python def insert(node, val): if not node: return TreeNode(val) if val < node.val: node.left = insert(node.left, val) elif val > node.val: node.right = insert(node.right, val) return node ``` ## PHP ```php function insert(?TreeNode $node, int $val): TreeNode { if ($node === null) return new TreeNode($val); if ($val < $node->val) $node->left = insert($node->left, $val); elseif ($val > $node->val) $node->right = insert($node->right, $val); return $node; } ``` ## Java ```java public static TreeNode insert(TreeNode node, int val) { if (node == null) { TreeNode fresh = new TreeNode(); fresh.val = val; return fresh; } if (val < node.val) node.left = insert(node.left, val); else if (val > node.val) node.right = insert(node.right, val); return node; } public static class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` ## C# ```csharp public static TreeNode Insert(TreeNode node, int val) { if (node == null) return new TreeNode { val = val }; if (val < node.val) node.left = Insert(node.left, val); else if (val > node.val) node.right = Insert(node.right, val); return node; } public class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` --- title: "Lowest common ancestor" url: https://algopath.pro/patterns/tree-lca language: en summary: "The lowest common ancestor is the deepest node that has both targets below it. One post-order pass over the tree finds it." updated: 2026-10-17 --- # Lowest common ancestor The lowest common ancestor is the deepest node that has both targets below it. One post-order pass over the tree finds it. ## How does Lowest common ancestor work? Ask every node the same question. Does either target sit inside my subtree? An empty node answers no. A node that is itself a target answers with itself. Otherwise ask both children. Each one returns a node or nothing. If both children answer with something, this node is the ancestor. The two targets split right here. If only one answers, pass that answer straight up. The split has not happened yet. The first node where both sides answer is the deepest one. Nothing above it can be lower. Sources: [Wikipedia: Lowest common ancestor](https://en.wikipedia.org/wiki/Lowest_common_ancestor) - `at node 6: found` Looking for 6 and 2. Node 6 answers with itself. - `at node 2: found` Node 2 also answers with itself. - `at node 5: both sides answered` The two targets split here. Node 5 is the answer. - `at node 3: only the left answered` The right subtree found nothing. Node 5 is passed up. - `answer = 5` One pass over the tree. No node was visited twice. ## When should you use Lowest common ancestor? Use Lowest common ancestor when the problem statement says “lowest common ancestor of two nodes”, “deepest node that is an ancestor of both p and q” or “each recursive call returns which target(s) it found below it”. A solution built on it usually runs in O(n). - lowest common ancestor of two nodes - deepest node that is an ancestor of both p and q - each recursive call returns which target(s) it found below it - path from root to each node then compare (alternate approach) - binary tree or BST given, find where the two paths split ## What is Lowest common ancestor confused with? Lowest common ancestor is most often confused with Binary search tree, Tree traversal, Union-find (disjoint set) and DP on trees. - **Binary search tree** - In a search tree the values alone point the way. In a plain tree both subtrees must be asked. - **Tree traversal** - That page is about the three visit orders. This uses post-order to answer one question. - **Union-find (disjoint set)** - Tarjan's offline algorithm answers many pairs with a disjoint set. A single pair does not need it. - **DP on trees** - DP on trees carries computed values upward. Here the thing carried up is only found or not found. ## What is the time and space complexity of Lowest common ancestor? The target complexity of Lowest common ancestor is O(n). n nodes give O(n) for one query. Many queries want binary lifting, at O(log n) each. ## A worked example of Lowest common ancestor ### Ancestor inside a search tree Both nodes live in a binary search tree. Find their lowest common ancestor. A node counts as an ancestor of itself here. The values themselves say which direction to walk. If both targets are smaller, go left; if both are larger, go right. The first node between them is the answer. ```javascript function lowestCommonAncestor(root, p, q) { let node = root; while (node) { if (p.val < node.val && q.val < node.val) { node = node.left; } else if (p.val > node.val && q.val > node.val) { node = node.right; } else { return node; // the values split here, or one of them is this node } } return null; } ``` ## Common mistakes with Lowest common ancestor - **Searching for each node separately** Two searches plus a path comparison is far more work. One post-order pass answers it directly. - **Forgetting a node can be its own ancestor** If one target sits above the other, it is the answer. Return the node as soon as it matches. - **Assuming both nodes are present** If one is missing, the pass returns the other one. Check presence when absence is allowed. - **Using the search-tree shortcut on a plain tree** Comparing values only works when the order is guaranteed. Otherwise both subtrees must be asked. ## Which interview problems use Lowest common ancestor? Interview problems solved with Lowest common ancestor include Lowest common ancestor of a binary tree, Lowest common ancestor of a BST, LCA with parent pointers, LCA of the deepest leaves, Distance between two nodes, Smallest subtree with all the deepest nodes and Step-by-step directions between nodes. - **Lowest common ancestor of a binary tree** The plain form: one post-order pass. - **Lowest common ancestor of a BST** The values alone decide the direction. - **LCA with parent pointers** Walk upward from both nodes and meet. - **LCA of the deepest leaves** Carry the depth up alongside the node. - **Distance between two nodes** Both depths minus twice the ancestor's depth. - **Smallest subtree with all the deepest nodes** The same shape with different targets. - **Step-by-step directions between nodes** Find the ancestor, then build both paths. ## JavaScript ```javascript function lca(node, p, q) { if (!node || node === p || node === q) return node; const left = lca(node.left, p, q); const right = lca(node.right, p, q); if (left && right) return node; // p and q split here return left || right; } ``` ## TypeScript ```typescript interface TreeNode { val: number; left: TreeNode | null; right: TreeNode | null; } function lca(node: TreeNode | null, p: TreeNode, q: TreeNode): TreeNode | null { if (!node || node === p || node === q) return node; const left = lca(node.left, p, q); const right = lca(node.right, p, q); if (left && right) return node; // p and q split here return left ?? right; } ``` ## Python ```python def lca(node, p, q): if not node or node is p or node is q: return node left = lca(node.left, p, q) right = lca(node.right, p, q) if left and right: return node # p and q split here return left or right ``` ## PHP ```php function lca(?TreeNode $node, TreeNode $p, TreeNode $q): ?TreeNode { if ($node === null || $node === $p || $node === $q) return $node; $left = lca($node->left, $p, $q); $right = lca($node->right, $p, $q); if ($left !== null && $right !== null) return $node; // split point return $left ?? $right; } ``` ## Java ```java public static TreeNode lca(TreeNode node, TreeNode p, TreeNode q) { if (node == null || node == p || node == q) return node; TreeNode left = lca(node.left, p, q); TreeNode right = lca(node.right, p, q); if (left != null && right != null) return node; // p and q split here return left != null ? left : right; } public static class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` ## C# ```csharp public static TreeNode Lca(TreeNode node, TreeNode p, TreeNode q) { if (node == null || node == p || node == q) return node; TreeNode left = Lca(node.left, p, q); TreeNode right = Lca(node.right, p, q); if (left != null && right != null) return node; // p and q split here return left ?? right; } public class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` --- title: "Tree serialize / deserialize" url: https://algopath.pro/patterns/tree-serialize language: en summary: "Write a tree out as one flat string, then rebuild it exactly. The markers for the missing children are what make it work." updated: 2026-10-17 --- # Tree serialize / deserialize Write a tree out as one flat string, then rebuild it exactly. The markers for the missing children are what make it work. ## How does Tree serialize / deserialize work? Pick one traversal order and stay with it. Pre-order is the easiest to rebuild from. Write each node's value as you visit it. Separate the values with a delimiter. Write a marker for every missing child too. Without those markers the shape is lost. To rebuild, read the tokens in that same order. Each token becomes exactly one node. A marker means an empty child, so return at once. A value builds a node and reads its two children. Both halves consume the tokens in the same order. That is why the shape comes back exactly. Sources: [Wikipedia: Serialization](https://en.wikipedia.org/wiki/Serialization) - `write 1` The tree is 1 with a left child 2. Pre-order writes the node first. - `write 2` The left child is written next. - `write #, #` Node 2 has no children. Two markers record that. - `write #` Node 1 has no right child either. - `1,2,#,#,#` Five tokens for two nodes. The markers carry the shape. ## When should you use Tree serialize / deserialize? Use Tree serialize / deserialize when the problem statement says “serialize a tree to a string and deserialize it back”, “rebuild the exact same shape, not just the same values” or “encode/decode wording, design a codec for a tree”. A solution built on it usually runs in O(n). - serialize a tree to a string and deserialize it back - rebuild the exact same shape, not just the same values - encode/decode wording, design a codec for a tree - null/None markers for missing children - store or transmit a tree (network transfer, file, cache) ## What is Tree serialize / deserialize confused with? Tree serialize / deserialize is most often confused with Tree traversal, Recursion, Graph BFS / DFS and Binary search tree. - **Tree traversal** - A traversal only reads the tree. Serialising also has to record where children are missing. - **Recursion** - Both halves are ordinary recursion. The real work is agreeing on a format. - **Graph BFS / DFS** - A level-order format is equally valid, and most puzzle sites use it. The reader has to match the writer. - **Binary search tree** - A search tree needs no markers, because the order fixes the shape. A plain tree does need them. ## What is the time and space complexity of Tree serialize / deserialize? The target complexity of Tree serialize / deserialize is O(n). n nodes give O(n) time and O(n) output. Each node and each missing child is written once. ## A worked example of Tree serialize / deserialize ### Serialise and rebuild a binary tree Turn a binary tree into a string. Then turn that string back into the same tree. The rebuilt tree has to match the original node for node. Write it in pre-order, with a hash for every empty child. Read it back with a cursor over the tokens. A hash returns null, anything else builds a node. ```javascript function serialize(root) { const out = []; (function write(node) { if (!node) { out.push("#"); // the marker is what preserves the shape return; } out.push(String(node.val)); write(node.left); write(node.right); })(root); return out.join(","); } function deserialize(data) { const tokens = data.split(","); let i = 0; function read() { const token = tokens[i++]; if (token === "#") return null; const node = new TreeNode(Number(token)); node.left = read(); // left must be consumed before right node.right = read(); return node; } return read(); } ``` ## Common mistakes with Tree serialize / deserialize - **Leaving out the markers** Pre-order alone cannot say where a child was missing. Two different trees then give one string. - **Picking a delimiter the data contains** Values can be negative or have several digits. Choose a character the values cannot hold. - **Rebuilding by index arithmetic** Index arithmetic only works on a complete tree. Use a moving cursor over the tokens instead. - **Reading the children in the wrong order** The reader has to consume left before right. Some languages evaluate arguments right to left. ## Which interview problems use Tree serialize / deserialize? Interview problems solved with Tree serialize / deserialize include Serialize and deserialize a binary tree, Serialize and deserialize a BST, Serialize and deserialize an n-ary tree, Build a tree from preorder and inorder, Build a tree from preorder and postorder, Find duplicate subtrees and Encode and decode strings. - **Serialize and deserialize a binary tree** The plain form, with a marker per empty child. - **Serialize and deserialize a BST** No markers needed, since the order fixes the shape. - **Serialize and deserialize an n-ary tree** Write the child count beside each value. - **Build a tree from preorder and inorder** Two traversals pin the shape without markers. - **Build a tree from preorder and postorder** The same idea, with one ambiguous case. - **Find duplicate subtrees** Serialise each subtree and count the strings. - **Encode and decode strings** The same length-prefix idea, applied to text. ## JavaScript ```javascript function serialize(node) { if (!node) return "#"; return `${node.val},${serialize(node.left)},${serialize(node.right)}`; } function deserialize(data) { const vals = data.split(","); let i = 0; function build() { if (vals[i] === "#") { i++; return null; } const node = { val: Number(vals[i++]), left: null, right: null }; node.left = build(); node.right = build(); return node; } return build(); } ``` ## TypeScript ```typescript interface TreeNode { val: number; left: TreeNode | null; right: TreeNode | null; } function serialize(node: TreeNode | null): string { if (!node) return "#"; return `${node.val},${serialize(node.left)},${serialize(node.right)}`; } function deserialize(data: string): TreeNode | null { const vals = data.split(","); let i = 0; function build(): TreeNode | null { if (vals[i] === "#") { i++; return null; } const node: TreeNode = { val: Number(vals[i++]), left: null, right: null }; node.left = build(); node.right = build(); return node; } return build(); } ``` ## Python ```python def serialize(node): if not node: return "#" return f"{node.val},{serialize(node.left)},{serialize(node.right)}" def deserialize(data): vals = iter(data.split(",")) def build(): val = next(vals) if val == "#": return None node = TreeNode(int(val)) node.left = build() node.right = build() return node return build() ``` ## PHP ```php function serialize(?TreeNode $node): string { if ($node === null) return "#"; return $node->val . "," . serialize($node->left) . "," . serialize($node->right); } function deserialize(array &$vals): ?TreeNode { $val = array_shift($vals); if ($val === "#") return null; $node = new TreeNode((int) $val); $node->left = deserialize($vals); $node->right = deserialize($vals); return $node; } ``` ## Java ```java public static String serialize(TreeNode node) { if (node == null) return "#"; return node.val + "," + serialize(node.left) + "," + serialize(node.right); } public static TreeNode deserialize(String data) { return build(new ArrayDeque<>(Arrays.asList(data.split(",")))); } private static TreeNode build(Deque vals) { String val = vals.poll(); if (val == null || val.equals("#")) return null; TreeNode node = new TreeNode(); node.val = Integer.parseInt(val); node.left = build(vals); node.right = build(vals); return node; } public static class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` ## C# ```csharp public static string Serialize(TreeNode node) { if (node == null) return "#"; return node.val + "," + Serialize(node.left) + "," + Serialize(node.right); } public static TreeNode Deserialize(string data) { return Build(new Queue(data.Split(','))); } private static TreeNode Build(Queue vals) { string val = vals.Dequeue(); if (val == "#") return null; var node = new TreeNode(); node.val = int.Parse(val); node.left = Build(vals); node.right = Build(vals); return node; } public class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` --- title: "Binary heap / priority queue" url: https://algopath.pro/patterns/binary-heap language: en summary: "A heap keeps the smallest value at the very top and puts nothing else in order at all. Pushing and popping each cost log n." updated: 2026-10-17 --- # Binary heap / priority queue A heap keeps the smallest value at the very top and puts nothing else in order at all. Pushing and popping each cost log n. ## How does Binary heap / priority queue work? A heap is a tree kept inside a flat array. The children of index i sit at 2i plus one and two. The only rule is that a parent beats its children. Siblings are in no order whatsoever. A push writes at the end, then bubbles the value up. It swaps with the parent while it wins. A pop takes the root, moves the last item up there, and sinks it. It swaps with the better child until it fits. Both moves walk one path of the tree. The height is log n, so both cost log n. Only the root ever means anything. Every other position is arbitrary. Sources: [Wikipedia: Binary heap](https://en.wikipedia.org/wiki/Binary_heap), [Python docs: heapq](https://docs.python.org/3/library/heapq.html) - `[5]` Pushing 5, 3 and 8. The first value is the root. - `[3, 5]` 3 is pushed and bubbles above the 5. - `[3, 5, 8]` 8 stays where it lands. It does not beat its parent. - `pop returns 3` The root is taken away. The last item, 8, moves to the top. - `[5, 8]` 8 sinks below the 5. The new root is the smallest left. ## When should you use Binary heap / priority queue? Use Binary heap / priority queue when the problem statement says “always need the current smallest/largest”, “top k elements” or “merge k sorted lists”. A solution built on it usually runs in O(log n) per op. - always need the current smallest/largest - top k elements - merge k sorted lists - running median while items keep arriving - priority queue / most urgent next ## What is Binary heap / priority queue confused with? Binary heap / priority queue is most often confused with Fast sort (merge / quick), Binary search tree, Monotonic deque (sliding window max/min) and Dijkstra's algorithm. - **Fast sort (merge / quick)** - Sorting gives a full order once. A heap gives the front repeatedly, while data still arrives. - **Binary search tree** - A search tree orders every pair of nodes. A heap only promises the root. - **Monotonic deque (sliding window max/min)** - A deque can drop the element that just expired. A heap has no cheap way to do that. - **Dijkstra's algorithm** - Dijkstra is the best-known user of a heap. This page is about the container itself. ## What is the time and space complexity of Binary heap / priority queue? The target complexity of Binary heap / priority queue is O(log n) per op. n up to 1e6 gives O(log n) per push or pop. Reading the top alone is O(1). ## A worked example of Binary heap / priority queue ### The k closest points to the origin Return the k points that lie closest to the origin. The list can be far too long to sort, and only k points are wanted. Keep a heap of size k whose top is the worst point still kept. Push each point, and pop the top whenever the heap grows past k. What remains is the answer. ```javascript function kClosest(points, k) { // a max-heap keyed by squared distance: the worst kept point sits on top const heap = new MaxHeap((p) => p[0] * p[0] + p[1] * p[1]); for (const point of points) { heap.push(point); if (heap.size() > k) { heap.pop(); // drops the farthest, never the closest } } return heap.toArray(); } ``` ## Common mistakes with Binary heap / priority queue - **Expecting a sorted array** Only the root is in any order. Reading the array from front to back gives nonsense. - **Keeping every element for a top-k question** A heap of size k costs O(n log k). Holding all of them costs more memory for no gain. - **Building the heap the wrong way round** For the k largest you need a min-heap, so the worst kept sits on top. That is easy to invert. - **Trying to delete an arbitrary element** A heap has no cheap way to find one. Mark it dead and skip it when it surfaces. ## Which interview problems use Binary heap / priority queue? Interview problems solved with Binary heap / priority queue include Kth largest element in a stream, K closest points to origin, Merge k sorted lists, Top k frequent elements, Task scheduler, Find median from a data stream and Dijkstra's shortest path. - **Kth largest element in a stream** A min-heap of size k, and the root is the answer. - **K closest points to origin** A bounded max-heap over squared distance. - **Merge k sorted lists** The heap holds the front node of every list. - **Top k frequent elements** Counts first, then a heap of size k. - **Task scheduler** The most frequent task goes first each round. - **Find median from a data stream** Two heaps facing each other across the middle. - **Dijkstra's shortest path** The heap decides which node to settle next. ## JavaScript ```javascript function kSmallest(nums, k) { const heap = []; // max-heap: keeps the k smallest seen so far const swap = (i, j) => ([heap[i], heap[j]] = [heap[j], heap[i]]); function push(v) { heap.push(v); let i = heap.length - 1; while (i > 0 && heap[(i - 1) >> 1] < heap[i]) { swap((i - 1) >> 1, i); i = (i - 1) >> 1; } } function pop() { const top = heap[0]; heap[0] = heap.pop(); let i = 0; while (2 * i + 1 < heap.length) { let c = 2 * i + 1; if (c + 1 < heap.length && heap[c + 1] > heap[c]) c++; if (heap[i] >= heap[c]) break; swap(i, c); i = c; } return top; } for (const x of nums) { push(x); if (heap.length > k) pop(); } return heap.slice().sort((a, b) => a - b); } ``` ## TypeScript ```typescript function kSmallest(nums: number[], k: number): number[] { const heap: number[] = []; // max-heap: keeps the k smallest seen so far const swap = (i: number, j: number): void => { [heap[i], heap[j]] = [heap[j], heap[i]]; }; function push(v: number): void { heap.push(v); let i = heap.length - 1; while (i > 0 && heap[(i - 1) >> 1] < heap[i]) { swap((i - 1) >> 1, i); i = (i - 1) >> 1; } } function pop(): number { const top = heap[0]; heap[0] = heap.pop()!; let i = 0; while (2 * i + 1 < heap.length) { let c = 2 * i + 1; if (c + 1 < heap.length && heap[c + 1] > heap[c]) c++; if (heap[i] >= heap[c]) break; swap(i, c); i = c; } return top; } for (const x of nums) { push(x); if (heap.length > k) pop(); } return heap.slice().sort((a, b) => a - b); } ``` ## Python ```python import heapq def k_smallest(nums, k): heap = [] # max-heap via negation: keeps the k smallest seen so far for x in nums: heapq.heappush(heap, -x) if len(heap) > k: heapq.heappop(heap) return sorted(-x for x in heap) ``` ## PHP ```php function kSmallest(array $nums, int $k): array { $heap = new SplMaxHeap(); // keeps the k smallest seen so far foreach ($nums as $x) { $heap->insert($x); if ($heap->count() > $k) { $heap->extract(); } } $result = []; foreach ($heap as $x) { $result[] = $x; } sort($result); return $result; } ``` ## Java ```java public static List kSmallest(int[] nums, int k) { // max-heap: keeps the k smallest seen so far PriorityQueue heap = new PriorityQueue<>(Comparator.reverseOrder()); for (int x : nums) { heap.add(x); if (heap.size() > k) heap.poll(); } List result = new ArrayList<>(heap); Collections.sort(result); return result; } ``` ## C# ```csharp public static List KSmallest(int[] nums, int k) { // max-heap via a negated key: keeps the k smallest seen so far var heap = new PriorityQueue(); foreach (int x in nums) { heap.Enqueue(x, -x); if (heap.Count > k) heap.Dequeue(); } var result = new List(); while (heap.Count > 0) result.Add(heap.Dequeue()); result.Sort(); return result; } ``` --- title: "Graph BFS / DFS" url: https://algopath.pro/patterns/graph-bfs-dfs language: en summary: "Walk a graph from a start node, marking everything you have seen. A queue gives the fewest hops, while a stack goes deep." updated: 2026-10-17 --- # Graph BFS / DFS Walk a graph from a start node, marking everything you have seen. A queue gives the fewest hops, while a stack goes deep. ## How does Graph BFS / DFS work? Put the start node in a container and mark it seen. The container is a queue or a stack. Take one node out of it. Look at every neighbour that node has. Skip any neighbour that is already marked. That check is what stops the walk from looping. Mark each new neighbour and put it in the container. Mark on entry, never on removal. A queue takes the oldest node, so the walk spreads level by level. That gives the fewest edges to each node. A stack takes the newest, so the walk drives deep first. Both visit every reachable node once. Sources: [Wikipedia: Breadth-first search](https://en.wikipedia.org/wiki/Breadth-first_search), [Wikipedia: Depth-first search](https://en.wikipedia.org/wiki/Depth-first_search) - `queue = [1], seen = {1}` Edges are 1-2, 1-3 and 2-4. The start is marked first. - `take 1, queue = [2, 3]` Both neighbours are new. They are marked as they enter. - `take 2, queue = [3, 4]` Node 4 is new. Node 1 is marked already and skipped. - `take 3, queue = [4]` Node 3 has no unmarked neighbours. - `take 4, queue = []` Nothing is left. Four nodes, each visited once. ## When should you use Graph BFS / DFS? Use Graph BFS / DFS when the problem statement says “shortest path in an unweighted graph”, “fewest steps/hops between nodes” or “can node X reach node Y”. A solution built on it usually runs in O(V+E). - shortest path in an unweighted graph - fewest steps/hops between nodes - can node X reach node Y - explore ring by ring / layer by layer - graph given as adjacency list, may contain cycles ## What is Graph BFS / DFS confused with? Graph BFS / DFS is most often confused with Tree traversal, Dijkstra's algorithm, Connected components and Backtracking. - **Tree traversal** - A tree cannot reach a node twice, so it needs no visited set. A graph can, so it always does. - **Dijkstra's algorithm** - Weighted edges break the level order. A heap then replaces the queue. - **Connected components** - That page counts the groups in a graph. This is the walk that each count is built from. - **Backtracking** - Backtracking undoes its state on the way out. A traversal marks a node and never unmarks it. ## What is the time and space complexity of Graph BFS / DFS? The target complexity of Graph BFS / DFS is O(V+E). V nodes and E edges give O(V + E). Memory is O(V) for the visited set and the frontier. ## A worked example of Graph BFS / DFS ### How long until every orange rots A grid holds empty cells, fresh oranges and rotten ones. Each minute a rotten orange rots its four neighbours. Return the minutes until nothing fresh is left, or minus one. Every rotten orange goes into the queue before the walk starts. Process one whole level of the queue per minute. A fresh orange left at the end means minus one. ```javascript function orangesRotting(grid) { const rows = grid.length; const cols = grid[0].length; let queue = []; let fresh = 0; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (grid[r][c] === 2) queue.push([r, c]); if (grid[r][c] === 1) fresh++; } } const steps = [[1, 0], [-1, 0], [0, 1], [0, -1]]; let minutes = 0; while (queue.length && fresh > 0) { const next = []; // one whole level per minute for (const [r, c] of queue) { for (const [dr, dc] of steps) { const nr = r + dr; const nc = c + dc; if (nr < 0 || nc < 0 || nr >= rows || nc >= cols) continue; if (grid[nr][nc] !== 1) continue; grid[nr][nc] = 2; // marked on entry, so it is queued once fresh--; next.push([nr, nc]); } } queue = next; minutes++; } return fresh === 0 ? minutes : -1; } ``` ## Common mistakes with Graph BFS / DFS - **Marking on removal instead of entry** The same node then enters the queue many times over. Mark it at the moment you push it. - **Using a stack when the answer is a distance** Depth-first can reach a node the long way round. Only a queue gives the fewest edges. - **Calling shift on a long queue** In JavaScript shift is O(n) on an array. Use a read index or a level list instead. - **Forgetting the graph can be disconnected** One walk only reaches one component. Loop over every node that is still unvisited. ## Which interview problems use Graph BFS / DFS? Interview problems solved with Graph BFS / DFS include Number of islands, Rotting oranges, Word ladder, Clone graph, Shortest path in a binary matrix, Course schedule and Pacific Atlantic water flow. - **Number of islands** One walk per unvisited land cell. - **Rotting oranges** Every rotten cell starts in the queue together. - **Word ladder** Words are nodes and one-letter edits are edges. - **Clone graph** The visited map also holds the copies made so far. - **Shortest path in a binary matrix** Eight directions, one queue, level counting. - **Course schedule** Detect a cycle in a directed graph. - **Pacific Atlantic water flow** Two walks, both started from the edges. ## JavaScript ```javascript function bfsDistances(adj, source) { const dist = { [source]: 0 }; const queue = [source]; while (queue.length) { const node = queue.shift(); for (const next of adj[node] || []) { if (!(next in dist)) { dist[next] = dist[node] + 1; queue.push(next); } } } return dist; } ``` ## TypeScript ```typescript function bfsDistances(adj: Record, source: string): Record { const dist: Record = { [source]: 0 }; const queue: string[] = [source]; while (queue.length) { const node = queue.shift()!; for (const next of adj[node] ?? []) { if (!(next in dist)) { dist[next] = dist[node] + 1; queue.push(next); } } } return dist; } ``` ## Python ```python from collections import deque def bfs_distances(adj, source): dist = {source: 0} queue = deque([source]) while queue: node = queue.popleft() for nxt in adj.get(node, []): if nxt not in dist: dist[nxt] = dist[node] + 1 queue.append(nxt) return dist ``` ## PHP ```php function bfsDistances(array $adj, $source): array { $dist = [$source => 0]; $queue = [$source]; while ($queue) { $node = array_shift($queue); foreach ($adj[$node] ?? [] as $next) { if (!isset($dist[$next])) { $dist[$next] = $dist[$node] + 1; $queue[] = $next; } } } return $dist; } ``` ## Java ```java public static Map bfsDistances(Map> adj, String source) { Map dist = new HashMap<>(); dist.put(source, 0); Queue queue = new ArrayDeque<>(); queue.add(source); while (!queue.isEmpty()) { String node = queue.poll(); for (String next : adj.getOrDefault(node, List.of())) { if (!dist.containsKey(next)) { dist.put(next, dist.get(node) + 1); queue.add(next); } } } return dist; } ``` ## C# ```csharp public static Dictionary BfsDistances(Dictionary> adj, string source) { var dist = new Dictionary { [source] = 0 }; var queue = new Queue(); queue.Enqueue(source); while (queue.Count > 0) { string node = queue.Dequeue(); if (!adj.TryGetValue(node, out var neighbours)) continue; foreach (string next in neighbours) { if (!dist.ContainsKey(next)) { dist[next] = dist[node] + 1; queue.Enqueue(next); } } } return dist; } ``` --- title: "Connected components" url: https://algopath.pro/patterns/graph-components language: en summary: "Start a new walk at every node you have not seen yet. Every walk that actually starts marks out one more connected group." updated: 2026-10-17 --- # Connected components Start a new walk at every node you have not seen yet. Every walk that actually starts marks out one more connected group. ## How does Connected components work? Keep one visited set for the entire run. It is never reset between walks. Loop over every node in the graph. Skip any node that is already marked. An unmarked node begins a new component. Add one to the counter. Run a full walk from that node. Everything it reaches belongs to this component. The walk marks everything it touches. So none of those can start a walk later. When the loop ends, the counter holds the number of groups. Every node was visited once. Sources: [Wikipedia: Component (graph theory)](https://en.wikipedia.org/wiki/Component_(graph_theory)) - `count = 0, visited = {}` Six nodes, with edges 0-1, 1-2 and 3-4. - `walk from 0` Node 0 is unmarked, so a component starts. The walk reaches 1 and 2. - `count = 1, visited = {0, 1, 2}` Nodes 1 and 2 are marked, so neither starts a walk. - `walk from 3, count = 2` Node 3 is unmarked. Its walk reaches node 4. - `walk from 5, count = 3` Node 5 has no edges. It is a component on its own. ## When should you use Connected components? Use Connected components when the problem statement says “count separate groups/islands/clusters”, “graph may be disconnected” or “how many connected pieces”. A solution built on it usually runs in O(V+E). - count separate groups/islands/clusters - graph may be disconnected - how many connected pieces - friend circles - label connected regions ## What is Connected components confused with? Connected components is most often confused with Graph BFS / DFS, Union-find (disjoint set), Bipartite check (two-coloring) and Topological sort (Kahn's algorithm). - **Graph BFS / DFS** - That is the walk itself. This page is the loop that starts one walk per group. - **Union-find (disjoint set)** - Union-find answers while the edges are still arriving. Flood fill needs the whole graph up front. - **Bipartite check (two-coloring)** - A two-colouring runs the same walk while carrying a colour. It answers a different question. - **Topological sort (Kahn's algorithm)** - Ordering needs directed edges and no cycles. Components treat every edge as undirected. ## What is the time and space complexity of Connected components? The target complexity of Connected components is O(V+E). V nodes and E edges give O(V + E) in total. Each node is visited by exactly one walk. ## A worked example of Connected components ### Count the islands in a grid A grid holds land cells and water cells. Count how many separate islands it contains. Cells only touch up, down, left and right. Walk the grid cell by cell. An unmarked land cell starts a new island. Flood the whole island from there, marking as you go. The number of floods is the answer. ```javascript function numIslands(grid) { let count = 0; function sink(r, c) { if (r < 0 || c < 0 || r >= grid.length || c >= grid[0].length) return; if (grid[r][c] !== "1") return; grid[r][c] = "0"; // marked in place, so no visited set is needed sink(r + 1, c); sink(r - 1, c); sink(r, c + 1); sink(r, c - 1); } for (let r = 0; r < grid.length; r++) { for (let c = 0; c < grid[0].length; c++) { if (grid[r][c] === "1") { count++; // one new island sink(r, c); } } } return count; } ``` ## Common mistakes with Connected components - **Resetting the visited set per walk** One component then gets counted many times. A single set covers the whole run. - **Counting nodes instead of walks** The answer is how many walks started. It is not how many nodes each one touched. - **Marking after descending** A cycle then sends the walk straight back before the mark exists. Mark before you descend. - **Recursing over a very large grid** A million-cell island overflows the call stack. Use an explicit stack or a queue. ## Which interview problems use Connected components? Interview problems solved with Connected components include Number of islands, Number of provinces, Connected components in an undirected graph, Max area of island, Count sub-islands, Making a large island and Redundant connection. - **Number of islands** One flood per unmarked land cell. - **Number of provinces** The graph arrives as an adjacency matrix. - **Connected components in an undirected graph** The plain form, over an edge list. - **Max area of island** Return the size of the largest walk, not the count. - **Count sub-islands** Count it only if every cell is land in the other grid too. - **Making a large island** Label each island, then test every water cell. - **Redundant connection** Union-find answers this while the edges arrive. ## JavaScript ```javascript function countComponents(n, adj) { const visited = new Set(); let count = 0; for (let start = 0; start < n; start++) { if (visited.has(start)) continue; count++; const stack = [start]; visited.add(start); while (stack.length) { const node = stack.pop(); for (const next of adj[node] || []) { if (!visited.has(next)) { visited.add(next); stack.push(next); } } } } return count; } ``` ## TypeScript ```typescript function countComponents(n: number, adj: number[][]): number { const visited = new Set(); let count = 0; for (let start = 0; start < n; start++) { if (visited.has(start)) continue; count++; const stack: number[] = [start]; visited.add(start); while (stack.length) { const node = stack.pop()!; for (const next of adj[node] ?? []) { if (!visited.has(next)) { visited.add(next); stack.push(next); } } } } return count; } ``` ## Python ```python def count_components(n, adj): visited = set() count = 0 for start in range(n): if start in visited: continue count += 1 stack = [start] visited.add(start) while stack: node = stack.pop() for nxt in adj.get(node, []): if nxt not in visited: visited.add(nxt) stack.append(nxt) return count ``` ## PHP ```php function countComponents(int $n, array $adj): int { $visited = []; $count = 0; for ($start = 0; $start < $n; $start++) { if (isset($visited[$start])) continue; $count++; $stack = [$start]; $visited[$start] = true; while ($stack) { $node = array_pop($stack); foreach ($adj[$node] ?? [] as $next) { if (!isset($visited[$next])) { $visited[$next] = true; $stack[] = $next; } } } } return $count; } ``` ## Java ```java public static int countComponents(int n, List> adj) { Set visited = new HashSet<>(); int count = 0; for (int start = 0; start < n; start++) { if (visited.contains(start)) continue; count++; Deque stack = new ArrayDeque<>(); stack.push(start); visited.add(start); while (!stack.isEmpty()) { int node = stack.pop(); for (int next : adj.get(node)) { if (visited.add(next)) stack.push(next); } } } return count; } ``` ## C# ```csharp public static int CountComponents(int n, List> adj) { var visited = new HashSet(); int count = 0; for (int start = 0; start < n; start++) { if (visited.Contains(start)) continue; count++; var stack = new Stack(); stack.Push(start); visited.Add(start); while (stack.Count > 0) { int node = stack.Pop(); foreach (int next in adj[node]) { if (visited.Add(next)) stack.Push(next); } } } return count; } ``` --- title: "Topological sort (Kahn's algorithm)" url: https://algopath.pro/patterns/topological-sort language: en summary: "Order the tasks so that every one of them comes after everything it depends on. Keep taking whichever node owes nothing." updated: 2026-10-17 --- # Topological sort (Kahn's algorithm) Order the tasks so that every one of them comes after everything it depends on. Keep taking whichever node owes nothing. ## How does Topological sort (Kahn's algorithm) work? Count how many edges point into each node. That number is its in-degree. Put every node with in-degree zero into a queue. Those owe nothing and can go first. Take one node out and append it to the answer. It is now placed for good. Walk its outgoing edges and drop each target's count by one. That target has one fewer debt. A count that reaches zero joins the queue. All of its dependencies are already placed. If the answer is shorter than the node count, a cycle exists. Nodes inside a cycle never reach zero. Sources: [Wikipedia: Topological sorting](https://en.wikipedia.org/wiki/Topological_sorting), [Kahn (1962), Topological sorting of large networks](https://doi.org/10.1145/368996.369025) - `in-degrees: A 0, B 1, C 1, D 2` Edges are A to B, A to C, B to D and C to D. - `queue = [A], output = []` Only A owes nothing, so it is the single starting point. - `take A, output = [A]` B and C each lose one debt. Both reach zero. - `queue = [B, C]` D still owes two. It has to wait. - `output = [A, B, C, D]` D joins once both of its debts are cleared. ## When should you use Topological sort (Kahn's algorithm)? Use Topological sort (Kahn's algorithm) when the problem statement says “task must come before another”, “order that respects dependencies” or “detect a cycle in prerequisites”. A solution built on it usually runs in O(V+E). - task must come before another - order that respects dependencies - detect a cycle in prerequisites - build order / course schedule / install order - directed graph, X must happen before Y ## What is Topological sort (Kahn's algorithm) confused with? Topological sort (Kahn's algorithm) is most often confused with Graph BFS / DFS, Connected components, Union-find (disjoint set) and Greedy (exchange argument). - **Graph BFS / DFS** - A plain walk visits every node in whatever order it likes. Here the order is the answer. - **Connected components** - Components ignore direction and only group nodes. Here direction defines the whole answer. - **Union-find (disjoint set)** - Union-find treats every edge as undirected. Direction is exactly what matters here. - **Greedy (exchange argument)** - The rule is greedy: take anything with no debts left. Removing a node can never add a dependency. ## What is the time and space complexity of Topological sort (Kahn's algorithm)? The target complexity of Topological sort (Kahn's algorithm) is O(V+E). V nodes and E edges give O(V + E). Every edge is removed exactly once. ## A worked example of Topological sort (Kahn's algorithm) ### An order to take every course You get a course count and a list of prerequisite pairs. Return an order that lets you take them all. If no such order exists, return an empty list. Build the adjacency list and the in-degree counts in one pass. Then run the queue loop. An answer shorter than the course count means a cycle. ```javascript function findOrder(numCourses, prerequisites) { const next = Array.from({ length: numCourses }, () => []); const indegree = new Array(numCourses).fill(0); for (const [course, needs] of prerequisites) { next[needs].push(course); indegree[course]++; } const queue = []; for (let i = 0; i < numCourses; i++) { if (indegree[i] === 0) queue.push(i); } const order = []; for (let i = 0; i < queue.length; i++) { // index walk, never shift const node = queue[i]; order.push(node); for (const target of next[node]) { if (--indegree[target] === 0) queue.push(target); } } // a short answer means some nodes never reached zero return order.length === numCourses ? order : []; } ``` ## Common mistakes with Topological sort (Kahn's algorithm) - **Building the edges backwards** A prerequisite pair reads as course then requirement. Reversing it silently solves a different problem. - **Not checking the length at the end** A cycle gives a short answer, never an error. Compare the length against the node count. - **Expecting one unique order** Several nodes can be ready at the same moment. Any order among them is correct. - **Using shift on the queue** In JavaScript that is O(n) per removal. Walk the array with an index instead. ## Which interview problems use Topological sort (Kahn's algorithm)? Interview problems solved with Topological sort (Kahn's algorithm) include Course schedule, Course schedule II, Alien dictionary, Minimum height trees, Sequence reconstruction, Parallel courses and Sort items by group. - **Course schedule** Only whether a valid order exists at all. - **Course schedule II** Return the order itself, not just a yes or no. - **Alien dictionary** Derive the edges by comparing adjacent words. - **Minimum height trees** Peel the leaves layer by layer instead. - **Sequence reconstruction** Check that exactly one node is ready at each step. - **Parallel courses** Count the rounds rather than producing an order. - **Sort items by group** Two orderings, one inside groups and one across them. ## JavaScript ```javascript function topoSort(n, adj) { const indeg = new Array(n).fill(0); for (const u in adj) for (const v of adj[u]) indeg[v]++; const queue = []; for (let i = 0; i < n; i++) if (indeg[i] === 0) queue.push(i); const order = []; while (queue.length) { const u = queue.shift(); order.push(u); for (const v of adj[u] || []) { if (--indeg[v] === 0) queue.push(v); } } return order.length === n ? order : null; // null: a cycle exists } ``` ## TypeScript ```typescript function topoSort(n: number, adj: number[][]): number[] | null { const indeg = new Array(n).fill(0); for (const edges of adj) for (const v of edges) indeg[v]++; const queue: number[] = []; for (let i = 0; i < n; i++) if (indeg[i] === 0) queue.push(i); const order: number[] = []; while (queue.length) { const u = queue.shift()!; order.push(u); for (const v of adj[u] ?? []) { if (--indeg[v] === 0) queue.push(v); } } return order.length === n ? order : null; // null: a cycle exists } ``` ## Python ```python from collections import deque def topo_sort(n, adj): indeg = [0] * n for u in adj: for v in adj[u]: indeg[v] += 1 queue = deque(i for i in range(n) if indeg[i] == 0) order = [] while queue: u = queue.popleft() order.append(u) for v in adj.get(u, []): indeg[v] -= 1 if indeg[v] == 0: queue.append(v) return order if len(order) == n else None # None: a cycle exists ``` ## PHP ```php function topoSort(int $n, array $adj): ?array { $indeg = array_fill(0, $n, 0); foreach ($adj as $u => $neighbors) { foreach ($neighbors as $v) $indeg[$v]++; } $queue = []; for ($i = 0; $i < $n; $i++) if ($indeg[$i] === 0) $queue[] = $i; $order = []; while ($queue) { $u = array_shift($queue); $order[] = $u; foreach ($adj[$u] ?? [] as $v) { if (--$indeg[$v] === 0) $queue[] = $v; } } return count($order) === $n ? $order : null; // null: a cycle exists } ``` ## Java ```java public static List topoSort(int n, List> adj) { int[] indeg = new int[n]; for (List edges : adj) for (int v : edges) indeg[v]++; Queue queue = new ArrayDeque<>(); for (int i = 0; i < n; i++) if (indeg[i] == 0) queue.add(i); List order = new ArrayList<>(); while (!queue.isEmpty()) { int u = queue.poll(); order.add(u); for (int v : adj.get(u)) { if (--indeg[v] == 0) queue.add(v); } } return order.size() == n ? order : null; // null: a cycle exists } ``` ## C# ```csharp public static List TopoSort(int n, List> adj) { var indeg = new int[n]; foreach (var edges in adj) foreach (int v in edges) indeg[v]++; var queue = new Queue(); for (int i = 0; i < n; i++) if (indeg[i] == 0) queue.Enqueue(i); var order = new List(); while (queue.Count > 0) { int u = queue.Dequeue(); order.Add(u); foreach (int v in adj[u]) { if (--indeg[v] == 0) queue.Enqueue(v); } } return order.Count == n ? order : null; // null: a cycle exists } ``` --- title: "Union-find (disjoint set)" url: https://algopath.pro/patterns/union-find language: en summary: "Every element points at a parent, and walking up reaches a root that names its group. Two elements match when their roots match." updated: 2026-10-22 --- # Union-find (disjoint set) Every element points at a parent, and walking up reaches a root that names its group. Two elements match when their roots match. ## How does Union-find (disjoint set) work? Union-find, disjoint set and DSU (disjoint set union) name the same structure. Each element starts as its own group. Its parent pointer points at itself. find walks up the parent chain until it reaches a root. The root is the group's name. union takes two elements, finds both roots, and hangs one under the other. One call merges two groups. Path compression rewrites every node on the walk to point straight at the root. The next find is one hop. Union by rank hangs the shorter tree under the taller one. That stops the chains from growing. With both, a find or a union costs near constant time. Nothing here needs the edges kept. Sources: [Wikipedia: Disjoint-set data structure](https://en.wikipedia.org/wiki/Disjoint-set_data_structure), [Tarjan (1975), Efficiency of a good but not linear set union algorithm](https://doi.org/10.1145/321879.321884) - `[0, 1, 2, 3, 4]` Five elements, five groups. Each one is its own parent. - `[0, 0, 2, 3, 4]` union(0, 1) hangs 1 under 0. Two elements now share a root. - `[0, 0, 2, 2, 4]` union(2, 3) hangs 3 under 2. A second group forms. - `[0, 0, 0, 2, 4]` union(1, 2) finds roots 0 and 2. Root 2 hangs under root 0. - `[0, 0, 0, 0, 4]` find(3) walks 3 to 2 to 0. Compression points 3 straight at 0. ## When should you use Union-find (disjoint set)? Use Union-find (disjoint set) when the problem statement says “are these two in the same group”, “merge/union facts arrive one at a time” or “dynamic connectivity as edges are added”. A solution built on it usually runs in near O(1) per op. - are these two in the same group - merge/union facts arrive one at a time - dynamic connectivity as edges are added - account/friend merging over time - adding this edge would create a cycle ## What is Union-find (disjoint set) confused with? Union-find (disjoint set) is most often confused with Connected components, Graph BFS / DFS, Topological sort and Hash set / map. - **Connected components** - Flood fill counts groups in a graph that is already complete. Union-find answers while the edges are still arriving. - **Graph BFS / DFS** - A traversal visits neighbours, so it needs an adjacency list. Union-find never stores the edges at all. - **Topological sort** - Topological order needs directed edges and a DAG. Union-find treats every edge as undirected. - **Hash set / map** - A map groups by a key you already know. Union-find discovers the groups as facts merge them. ## What is the time and space complexity of Union-find (disjoint set)? The target complexity of Union-find (disjoint set) is near O(1) per op. n up to 1e6 elements with m interleaved queries gives O(m alpha(n)). Alpha is under 5 for any n you will meet. ## A worked example of Union-find (disjoint set) ### Redundant connection You get a graph that was a tree, plus one extra edge. Find the edge that closes a cycle. Edges arrive in order. Return the last one joining two nodes already connected. Walk the edges in order and union each one. When both ends already share a root, the edge adds nothing. It closes a cycle. Keep the last such edge. That is the answer. ```javascript function findRedundantConnection(edges) { const parent = Array.from({ length: edges.length + 1 }, (_, i) => i); function find(x) { while (parent[x] !== x) { parent[x] = parent[parent[x]]; // path halving x = parent[x]; } return x; } let answer = []; for (const [a, b] of edges) { const rootA = find(a); const rootB = find(b); if (rootA === rootB) { answer = [a, b]; // both ends were already connected } else { parent[rootA] = rootB; } } return answer; } ``` ## Common mistakes with Union-find (disjoint set) - **Comparing elements instead of roots** Two elements can sit in one group with different parents. Compare find(a) with find(b). - **Union without find** Writing parent[a] = b joins two elements, not two groups. Always hang one root under the other. - **Skipping path compression** Without it a chain can grow to n links. A single find then costs O(n). - **Sizing the array by edge count** The array is indexed by element, not by edge. An off-by-one here reads undefined for the last node. ## Which interview problems use Union-find (disjoint set)? Interview problems solved with Union-find (disjoint set) include Number of provinces, Redundant connection, Accounts merge, Number of islands, Most stones removed, Satisfiability of equality equations, Smallest string with swaps and Min cost to connect all points. - **Number of provinces** Union every connected pair, then count the distinct roots. - **Redundant connection** The first edge whose two ends already share a root. - **Accounts merge** Emails link accounts. Union on a shared email, then group by root. - **Number of islands** Union neighbouring land cells. Useful when the grid arrives in pieces. - **Most stones removed** Stones sharing a row or a column belong to one group. - **Satisfiability of equality equations** Union the equals first. Then test every not-equals against the roots. - **Smallest string with swaps** Swappable indices form a group. Sort the letters inside each group. - **Min cost to connect all points** Kruskal's algorithm: sort edges by cost, keep each edge whose ends have different roots. ## JavaScript ```javascript class UnionFind { constructor(n) { this.parent = Array.from({ length: n }, (_, i) => i); this.rank = new Array(n).fill(0); } find(x) { if (this.parent[x] !== x) this.parent[x] = this.find(this.parent[x]); return this.parent[x]; } union(x, y) { const rx = this.find(x), ry = this.find(y); if (rx === ry) return false; if (this.rank[rx] < this.rank[ry]) this.parent[rx] = ry; else if (this.rank[rx] > this.rank[ry]) this.parent[ry] = rx; else { this.parent[ry] = rx; this.rank[rx]++; } return true; } } ``` ## TypeScript ```typescript class UnionFind { private parent: number[]; private rank: number[]; constructor(n: number) { this.parent = Array.from({ length: n }, (_, i) => i); this.rank = new Array(n).fill(0); } find(x: number): number { if (this.parent[x] !== x) this.parent[x] = this.find(this.parent[x]); return this.parent[x]; } union(x: number, y: number): boolean { const rx = this.find(x); const ry = this.find(y); if (rx === ry) return false; if (this.rank[rx] < this.rank[ry]) this.parent[rx] = ry; else if (this.rank[rx] > this.rank[ry]) this.parent[ry] = rx; else { this.parent[ry] = rx; this.rank[rx]++; } return true; } } ``` ## Python ```python class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.rank = [0] * n def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): rx, ry = self.find(x), self.find(y) if rx == ry: return False if self.rank[rx] < self.rank[ry]: rx, ry = ry, rx self.parent[ry] = rx if self.rank[rx] == self.rank[ry]: self.rank[rx] += 1 return True ``` ## PHP ```php class UnionFind { private array $parent; private array $rank; public function __construct(int $n) { $this->parent = range(0, $n - 1); $this->rank = array_fill(0, $n, 0); } public function find(int $x): int { if ($this->parent[$x] !== $x) { $this->parent[$x] = $this->find($this->parent[$x]); } return $this->parent[$x]; } public function union(int $x, int $y): bool { [$rx, $ry] = [$this->find($x), $this->find($y)]; if ($rx === $ry) return false; if ($this->rank[$rx] < $this->rank[$ry]) [$rx, $ry] = [$ry, $rx]; $this->parent[$ry] = $rx; if ($this->rank[$rx] === $this->rank[$ry]) $this->rank[$rx]++; return true; } } ``` ## Java ```java public static class UnionFind { private final int[] parent; private final int[] rank; public UnionFind(int n) { parent = new int[n]; rank = new int[n]; for (int i = 0; i < n; i++) parent[i] = i; } public int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); return parent[x]; } public boolean union(int x, int y) { int rx = find(x), ry = find(y); if (rx == ry) return false; if (rank[rx] < rank[ry]) parent[rx] = ry; else if (rank[rx] > rank[ry]) parent[ry] = rx; else { parent[ry] = rx; rank[rx]++; } return true; } } ``` ## C# ```csharp public class UnionFind { private readonly int[] parent; private readonly int[] rank; public UnionFind(int n) { parent = new int[n]; rank = new int[n]; for (int i = 0; i < n; i++) parent[i] = i; } public int Find(int x) { if (parent[x] != x) parent[x] = Find(parent[x]); return parent[x]; } public bool Union(int x, int y) { int rx = Find(x), ry = Find(y); if (rx == ry) return false; if (rank[rx] < rank[ry]) parent[rx] = ry; else if (rank[rx] > rank[ry]) parent[ry] = rx; else { parent[ry] = rx; rank[rx]++; } return true; } } ``` --- title: "Dijkstra's algorithm" url: https://algopath.pro/patterns/dijkstra language: en summary: "Settle the closest node not yet settled, then relax all of its edges. A heap keeps that node always one single read away." updated: 2026-10-17 --- # Dijkstra's algorithm Settle the closest node not yet settled, then relax all of its edges. A heap keeps that node always one single read away. ## How does Dijkstra's algorithm work? Give every node a distance of infinity. The start node gets zero instead. Put the start into a heap keyed by distance. The heap always hands back the nearest node. Pop a node from the heap. If its stored distance is better than the popped one, skip it. Otherwise the node is settled. Its distance is final and can never improve again. For each outgoing edge, compare the stored distance with the route through this node. If the new one is shorter, store it and push it. Repeat until the heap runs empty. Every reachable node has been settled once. Sources: [Wikipedia: Dijkstra's algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm), [Dijkstra (1959), A note on two problems in connexion with graphs](https://doi.org/10.1007/BF01386390) - `dist = A 0, B inf, C inf` Three edges: A to B at 1, A to C at 4. B reaches C at 2. - `pop A and relax` B becomes 1 and C becomes 4. - `pop B at 1, relax to C` Zero plus 1 plus 2 is 3, which beats the 4. - `dist = A 0, B 1, C 3` C is pushed again with the better distance. - `pop C at 3, then C at 4` The stale entry is skipped, since 3 is already better. ## When should you use Dijkstra's algorithm? Use Dijkstra's algorithm when the problem statement says “cheapest/shortest path with weighted edges”, “non-negative edge weights” or “minimum total cost to reach every node”. A solution built on it usually runs in O(E log V). - cheapest/shortest path with weighted edges - non-negative edge weights - minimum total cost to reach every node - network delay time / route cost, not fewest roads - weighted graph, single-source shortest paths ## What is Dijkstra's algorithm confused with? Dijkstra's algorithm is most often confused with Graph BFS / DFS, Binary heap / priority queue, Greedy (exchange argument) and Topological sort (Kahn's algorithm). - **Graph BFS / DFS** - A queue counts hops, so it assumes every edge costs the same. A heap handles real weights. - **Binary heap / priority queue** - The heap is the tool this runs on. That page is about the container itself. - **Greedy (exchange argument)** - Settling the nearest node is a greedy choice. It only holds while no weight is negative. - **Topological sort (Kahn's algorithm)** - On a graph with no cycles the order relaxes edges without a heap. That is faster and allows negatives. ## What is the time and space complexity of Dijkstra's algorithm? The target complexity of Dijkstra's algorithm is O(E log V). V nodes and E edges give O(E log V) with a heap. Negative weights break it, so use Bellman-Ford. ## A worked example of Dijkstra's algorithm ### How long a signal takes to reach everyone A signal starts at one node and travels along weighted edges. Return the time until every node has it. If some node cannot be reached at all, return minus one. The signal problem is one shortest-path run from a single source. The answer is the largest of the final distances. A node never reached leaves an infinity behind. ```javascript function networkDelayTime(times, n, k) { const graph = new Map(); for (const [from, to, weight] of times) { if (!graph.has(from)) graph.set(from, []); graph.get(from).push([to, weight]); } const dist = new Map([[k, 0]]); const heap = new MinHeap((entry) => entry[0]); // entries are [distance, node] heap.push([0, k]); while (heap.size()) { const [d, node] = heap.pop(); if (d > (dist.get(node) ?? Infinity)) continue; // a stale entry for (const [next, weight] of graph.get(node) ?? []) { const candidate = d + weight; if (candidate < (dist.get(next) ?? Infinity)) { dist.set(next, candidate); heap.push([candidate, next]); } } } if (dist.size < n) return -1; return Math.max(...dist.values()); } ``` ## Common mistakes with Dijkstra's algorithm - **Running it with negative weights** A settled node could still improve, so the greedy step fails. Use Bellman-Ford instead. - **Not skipping stale heap entries** A node can be pushed several times with different distances. Compare against the stored value on pop. - **Marking a node settled on push** A node pushed early can get a better route later. Settle it on pop, never on push. - **Using a plain queue** A queue counts edges rather than weight. That only matches when every edge costs the same. ## Which interview problems use Dijkstra's algorithm? Interview problems solved with Dijkstra's algorithm include Network delay time, Cheapest flights within k stops, Path with minimum effort, Swim in rising water, Path with maximum probability, Minimum cost to reach the last cell and Second shortest path. - **Network delay time** The plain form: the answer is the largest distance. - **Cheapest flights within k stops** A stop limit adds a second dimension to the state. - **Path with minimum effort** The cost of a path is its single worst step. - **Swim in rising water** The same, with a maximum instead of a sum. - **Path with maximum probability** Multiply instead of adding, and keep the largest. - **Minimum cost to reach the last cell** A grid is a graph with four edges per cell. - **Second shortest path** Keep the two best distances for each node. ## JavaScript ```javascript function dijkstra(n, adj, source) { const dist = new Array(n).fill(Infinity); dist[source] = 0; const heap = [[0, source]]; // [distance, node] while (heap.length) { heap.sort((a, b) => a[0] - b[0]); // swap for a real heap in production const [d, u] = heap.shift(); if (d > dist[u]) continue; for (const [v, w] of adj[u] || []) { if (d + w < dist[v]) { dist[v] = d + w; heap.push([dist[v], v]); } } } return dist; } ``` ## TypeScript ```typescript function dijkstra(n: number, adj: Array>, source: number): number[] { const dist = new Array(n).fill(Infinity); dist[source] = 0; const heap: Array<[number, number]> = [[0, source]]; // [distance, node] while (heap.length) { heap.sort((a, b) => a[0] - b[0]); // swap for a real heap in production const [d, u] = heap.shift()!; if (d > dist[u]) continue; for (const [v, w] of adj[u] ?? []) { if (d + w < dist[v]) { dist[v] = d + w; heap.push([dist[v], v]); } } } return dist; } ``` ## Python ```python import heapq def dijkstra(n, adj, source): dist = [float('inf')] * n dist[source] = 0 heap = [(0, source)] while heap: d, u = heapq.heappop(heap) if d > dist[u]: continue for v, w in adj.get(u, []): if d + w < dist[v]: dist[v] = d + w heapq.heappush(heap, (dist[v], v)) return dist ``` ## PHP ```php function dijkstra(int $n, array $adj, int $source): array { $dist = array_fill(0, $n, INF); $dist[$source] = 0; $heap = new SplPriorityQueue(); $heap->insert($source, 0); while (!$heap->isEmpty()) { $u = $heap->extract(); foreach ($adj[$u] ?? [] as [$v, $w]) { if ($dist[$u] + $w < $dist[$v]) { $dist[$v] = $dist[$u] + $w; $heap->insert($v, -$dist[$v]); // max-heap: negate for min-order } } } return $dist; } ``` ## Java ```java public static int[] dijkstra(int n, List> adj, int source) { int[] dist = new int[n]; Arrays.fill(dist, Integer.MAX_VALUE); dist[source] = 0; // {distance, node}, smallest distance first PriorityQueue heap = new PriorityQueue<>(Comparator.comparingInt(a -> a[0])); heap.add(new int[] { 0, source }); while (!heap.isEmpty()) { int[] top = heap.poll(); int d = top[0], u = top[1]; if (d > dist[u]) continue; for (int[] edge : adj.get(u)) { int v = edge[0], w = edge[1]; if (d + w < dist[v]) { dist[v] = d + w; heap.add(new int[] { dist[v], v }); } } } return dist; } ``` ## C# ```csharp public static int[] Dijkstra(int n, List> adj, int source) { var dist = new int[n]; Array.Fill(dist, int.MaxValue); dist[source] = 0; // the node, keyed by its distance: smallest first var heap = new PriorityQueue(); heap.Enqueue(source, 0); while (heap.Count > 0) { heap.TryDequeue(out int u, out int d); if (d > dist[u]) continue; foreach (var edge in adj[u]) { int v = edge[0], w = edge[1]; if (d + w < dist[v]) { dist[v] = d + w; heap.Enqueue(v, dist[v]); } } } return dist; } ``` --- title: "Eulerian path (Hierholzer's algorithm)" url: https://algopath.pro/patterns/eulerian-path language: en summary: "Walk a graph using every one of its edges exactly once. Push a node the moment it gets stuck, then read the answer backwards." updated: 2026-10-17 --- # Eulerian path (Hierholzer's algorithm) Walk a graph using every one of its edges exactly once. Push a node the moment it gets stuck, then read the answer backwards. ## How does Eulerian path (Hierholzer's algorithm) work? Check the degrees before anything else. An undirected path allows at most two nodes with an odd degree. In a directed graph, at most one node may have an extra outgoing edge. That node has to be the start. Walk forward greedily and delete each edge as you use it. A used edge is never walked again. When a node has no edges left, push it onto an output stack. It is stuck, so it belongs near the end. Step back to the previous node and carry on. Any unused edges there are still reachable. Reverse the output stack at the very end. That puts the path in the right order. Sources: [Wikipedia: Eulerian path](https://en.wikipedia.org/wiki/Eulerian_path) - `at JFK, edges to A and B` Take A first when the smallest route is wanted. - `at A, then back to JFK` The only edge out of A returns to JFK. - `at B: no edges left` B is stuck, so it is pushed first. - `push JFK, A, JFK` Each node is pushed once it runs dry. - `reversed: JFK, A, JFK, B` Reading the stack backwards gives the route. ## When should you use Eulerian path (Hierholzer's algorithm)? Use Eulerian path (Hierholzer's algorithm) when the problem statement says “use every edge exactly once”, “reconstruct an itinerary from tickets” or “one-stroke drawing / draw without lifting the pen”. A solution built on it usually runs in O(E). - use every edge exactly once - reconstruct an itinerary from tickets - one-stroke drawing / draw without lifting the pen - walk every road/domino exactly once - flights or dominoes must chain into one trip ## What is Eulerian path (Hierholzer's algorithm) confused with? Eulerian path (Hierholzer's algorithm) is most often confused with Graph BFS / DFS, Backtracking, Connected components and Stack (LIFO). - **Graph BFS / DFS** - A traversal has to reach every node. This one has to use every edge. - **Backtracking** - Trying routes and undoing them is exponential. This walk never has to take a step back. - **Connected components** - Every edge must sit in one component, which is one of the conditions here. Counting groups is not the goal. - **Stack (LIFO)** - The stack is what makes the walk work. This page is about when to push onto it. ## What is the time and space complexity of Eulerian path (Hierholzer's algorithm)? The target complexity of Eulerian path (Hierholzer's algorithm) is O(E). V nodes and E edges give O(E log E) once the edges are sorted. Without sorting it is O(E). ## A worked example of Eulerian path (Hierholzer's algorithm) ### Rebuild a travel itinerary You get a list of flight tickets. Every ticket must be used exactly once, starting at JFK. When several routes exist, return the smallest in alphabetical order. Sort each airport's destinations so the smallest is taken first. Walk greedily and push an airport once it has no tickets left. Reverse the stack at the end. ```javascript function findItinerary(tickets) { const graph = new Map(); for (const [from, to] of tickets) { if (!graph.has(from)) graph.set(from, []); graph.get(from).push(to); } // reversed, so pop() hands back the smallest destination for (const list of graph.values()) list.sort().reverse(); const route = []; const stack = ["JFK"]; while (stack.length) { const airport = stack[stack.length - 1]; const next = graph.get(airport); if (next && next.length) { stack.push(next.pop()); // the ticket is used up } else { route.push(stack.pop()); // stuck, so this airport ends the route } } return route.reverse(); } ``` ## Common mistakes with Eulerian path (Hierholzer's algorithm) - **Returning the path in walk order** The output stack has to be reversed. Walk order puts the dead ends in the wrong place. - **Not deleting the edge you used** The same edge is then walked forever. Remove it at the moment you take it. - **Skipping the degree check** Plenty of graphs have no such walk at all. Test the degrees before starting. - **Reaching for backtracking** Trying and undoing routes explodes on a large graph. The stack rule never needs a retry. ## Which interview problems use Eulerian path (Hierholzer's algorithm)? Interview problems solved with Eulerian path (Hierholzer's algorithm) include Reconstruct itinerary, Valid arrangement of pairs, Cracking the safe, Eulerian circuit check, Domino chain, Rebuild a genome from fragments and The bridges of Konigsberg. - **Reconstruct itinerary** Directed edges, with alphabetical order as the tie-break. - **Valid arrangement of pairs** The start is decided by the degrees. - **Cracking the safe** A de Bruijn sequence is an Eulerian circuit. - **Eulerian circuit check** Every degree even, and all edges in one component. - **Domino chain** Each domino is an edge between two numbers. - **Rebuild a genome from fragments** The overlap graph carries an Eulerian path. - **The bridges of Konigsberg** The original proof that no such walk exists. ## JavaScript ```javascript function eulerianPath(tickets, start) { const adj = {}; for (const [from, to] of tickets) { (adj[from] ||= []).push(to); } for (const from in adj) adj[from].sort().reverse(); // pop smallest first const route = []; const stack = [start]; while (stack.length) { const node = stack[stack.length - 1]; if (adj[node] && adj[node].length) { stack.push(adj[node].pop()); } else { route.push(stack.pop()); // dead end: record and back up } } return route.reverse(); } ``` ## TypeScript ```typescript function eulerianPath(tickets: string[][], start: string): string[] { const adj: Record = {}; for (const [from, to] of tickets) { (adj[from] ??= []).push(to); } for (const from in adj) adj[from].sort().reverse(); // pop smallest first const route: string[] = []; const stack: string[] = [start]; while (stack.length) { const dests = adj[stack[stack.length - 1]] ?? []; if (dests.length) { stack.push(dests.pop()!); } else { route.push(stack.pop()!); // dead end: record and back up } } return route.reverse(); } ``` ## Python ```python def eulerian_path(tickets, start): adj = {} for frm, to in tickets: adj.setdefault(frm, []).append(to) for frm in adj: adj[frm].sort(reverse=True) # pop smallest first route = [] stack = [start] while stack: node = stack[-1] if adj.get(node): stack.append(adj[node].pop()) else: route.append(stack.pop()) # dead end: record and back up return route[::-1] ``` ## PHP ```php function eulerianPath(array $tickets, string $start): array { $adj = []; foreach ($tickets as [$from, $to]) { $adj[$from][] = $to; } foreach ($adj as $from => &$list) { rsort($list); // pop smallest first } unset($list); $route = []; $stack = [$start]; while ($stack) { $node = end($stack); if (!empty($adj[$node])) { $stack[] = array_pop($adj[$node]); } else { $route[] = array_pop($stack); // dead end: record and back up } } return array_reverse($route); } ``` ## Java ```java public static List eulerianPath(String[][] tickets, String start) { Map> adj = new HashMap<>(); for (String[] ticket : tickets) { adj.computeIfAbsent(ticket[0], k -> new ArrayList<>()).add(ticket[1]); } for (List dests : adj.values()) { dests.sort(Comparator.reverseOrder()); // pop smallest first } List route = new ArrayList<>(); Deque stack = new ArrayDeque<>(); stack.push(start); while (!stack.isEmpty()) { List dests = adj.get(stack.peek()); if (dests != null && !dests.isEmpty()) { stack.push(dests.remove(dests.size() - 1)); } else { route.add(stack.pop()); // dead end: record and back up } } Collections.reverse(route); return route; } ``` ## C# ```csharp public static List EulerianPath(string[][] tickets, string start) { var adj = new Dictionary>(); foreach (var ticket in tickets) { if (!adj.ContainsKey(ticket[0])) adj[ticket[0]] = new List(); adj[ticket[0]].Add(ticket[1]); } foreach (var dests in adj.Values) { dests.Sort(); dests.Reverse(); // pop smallest first } var route = new List(); var stack = new Stack(); stack.Push(start); while (stack.Count > 0) { if (adj.TryGetValue(stack.Peek(), out var dests) && dests.Count > 0) { string next = dests[dests.Count - 1]; dests.RemoveAt(dests.Count - 1); stack.Push(next); } else { route.Add(stack.Pop()); // dead end: record and back up } } route.Reverse(); return route; } ``` --- title: "Bipartite check (two-coloring)" url: https://algopath.pro/patterns/bipartite language: en summary: "Colour any one node, then colour each of its neighbours in the other colour. A conflict proves the graph is not bipartite." updated: 2026-10-17 --- # Bipartite check (two-coloring) Colour any one node, then colour each of its neighbours in the other colour. A conflict proves the graph is not bipartite. ## How does Bipartite check (two-coloring) work? Start with every node uncoloured. Then loop over all of them. An uncoloured node begins a new walk. Paint it with the first colour. Visit each neighbour in turn. Paint an uncoloured neighbour with the opposite colour. A neighbour that already has a colour must differ from the current node. If they match, the graph fails. A conflict means an odd cycle exists somewhere. No two-colouring can survive one of those. Repeat for every component. The graph is bipartite only when all of them pass. Sources: [Wikipedia: Bipartite graph](https://en.wikipedia.org/wiki/Bipartite_graph) - `colour[0] = A` The graph is a triangle: 0-1, 1-2 and 2-0. - `colour[1] = B` Its neighbour takes the other colour. - `colour[2] = A` Node 2 sits next to node 1, so it takes A. - `edge 2-0: A against A` Both ends carry the same colour. The check fails. - `answer = false` A triangle is an odd cycle. Two colours can never fit it. ## When should you use Bipartite check (two-coloring)? Use Bipartite check (two-coloring) when the problem statement says “split into two groups with no conflict inside a group”, “two-color the graph” or “detect an odd-length cycle”. A solution built on it usually runs in O(V+E). - split into two groups with no conflict inside a group - two-color the graph - detect an odd-length cycle - us vs them / two-shift scheduling / two-room assignment - possible bipartition of conflict pairs ## What is Bipartite check (two-coloring) confused with? Bipartite check (two-coloring) is most often confused with Graph BFS / DFS, Connected components, Union-find (disjoint set) and Backtracking. - **Graph BFS / DFS** - The walk itself is identical. The only addition is a colour carried on each node. - **Connected components** - Both loop over unvisited nodes and start a walk. Here a walk can also fail. - **Union-find (disjoint set)** - A weighted disjoint set answers the same question. It suits edges that arrive over time. - **Backtracking** - Three or more colours needs a search with undo. Two colours never needs a choice. ## What is the time and space complexity of Bipartite check (two-coloring)? The target complexity of Bipartite check (two-coloring) is O(V+E). V nodes and E edges give O(V + E). Each node is coloured once and each edge checked twice. ## A worked example of Bipartite check (two-coloring) ### Split people into two groups You get a number of people and a list of pairs who dislike each other. Split everyone into two groups so that nobody shares a group with someone they dislike. Each person is a node and each dislike is an edge. Run the two-colouring over every component. A single conflict makes the split impossible. ```javascript function possibleBipartition(n, dislikes) { const graph = Array.from({ length: n + 1 }, () => []); for (const [a, b] of dislikes) { graph[a].push(b); graph[b].push(a); } const colour = new Array(n + 1).fill(0); for (let start = 1; start <= n; start++) { if (colour[start] !== 0) continue; // already placed by an earlier walk colour[start] = 1; const queue = [start]; for (let i = 0; i < queue.length; i++) { const node = queue[i]; for (const next of graph[node]) { if (colour[next] === colour[node]) return false; // same side if (colour[next] === 0) { colour[next] = -colour[node]; queue.push(next); } } } } return true; } ``` ## Common mistakes with Bipartite check (two-coloring) - **Walking only from the first node** A disconnected graph can hide the conflict elsewhere. Start a walk from every uncoloured node. - **Storing only a visited flag** You need the colour, not just whether it was seen. One array can carry both. - **Colouring on removal from the queue** The same node can then enter twice with different colours. Colour it as you push it. - **Expecting failure to look specific** The only possible cause is an odd cycle. A graph with only even cycles always passes. ## Which interview problems use Bipartite check (two-coloring)? Interview problems solved with Bipartite check (two-coloring) include Is graph bipartite, Possible bipartition, Divide players into two teams, Detect an odd cycle, Maximum bipartite matching, Flower planting with no adjacent same and Graph colouring with three colours. - **Is graph bipartite** The plain form, over an adjacency list. - **Possible bipartition** Dislike pairs become the edges. - **Divide players into two teams** The same two-colouring, worded differently. - **Detect an odd cycle** The same test, asked the other way round. - **Maximum bipartite matching** Only meaningful once the two sides are known. - **Flower planting with no adjacent same** Four colours, so a greedy pass is enough. - **Graph colouring with three colours** Backtracking, since two colours no longer decide it. ## JavaScript ```javascript function isBipartite(n, adj) { const color = new Array(n).fill(-1); for (let start = 0; start < n; start++) { if (color[start] !== -1) continue; color[start] = 0; const queue = [start]; while (queue.length) { const node = queue.shift(); for (const next of adj[node] || []) { if (color[next] === -1) { color[next] = 1 - color[node]; queue.push(next); } else if (color[next] === color[node]) { return false; // same color on both ends: odd cycle } } } } return true; } ``` ## TypeScript ```typescript function isBipartite(n: number, adj: number[][]): boolean { const color = new Array(n).fill(-1); for (let start = 0; start < n; start++) { if (color[start] !== -1) continue; color[start] = 0; const queue: number[] = [start]; while (queue.length) { const node = queue.shift()!; for (const next of adj[node] ?? []) { if (color[next] === -1) { color[next] = 1 - color[node]; queue.push(next); } else if (color[next] === color[node]) { return false; // same color on both ends: odd cycle } } } } return true; } ``` ## Python ```python from collections import deque def is_bipartite(n, adj): color = [-1] * n for start in range(n): if color[start] != -1: continue color[start] = 0 queue = deque([start]) while queue: node = queue.popleft() for nxt in adj.get(node, []): if color[nxt] == -1: color[nxt] = 1 - color[node] queue.append(nxt) elif color[nxt] == color[node]: return False # same color on both ends: odd cycle return True ``` ## PHP ```php function isBipartite(int $n, array $adj): bool { $color = array_fill(0, $n, -1); for ($start = 0; $start < $n; $start++) { if ($color[$start] !== -1) continue; $color[$start] = 0; $queue = [$start]; while ($queue) { $node = array_shift($queue); foreach ($adj[$node] ?? [] as $next) { if ($color[$next] === -1) { $color[$next] = 1 - $color[$node]; $queue[] = $next; } elseif ($color[$next] === $color[$node]) { return false; // same color on both ends: odd cycle } } } } return true; } ``` ## Java ```java public static boolean isBipartite(int n, List> adj) { int[] color = new int[n]; Arrays.fill(color, -1); for (int start = 0; start < n; start++) { if (color[start] != -1) continue; color[start] = 0; Queue queue = new ArrayDeque<>(); queue.add(start); while (!queue.isEmpty()) { int node = queue.poll(); for (int next : adj.get(node)) { if (color[next] == -1) { color[next] = 1 - color[node]; queue.add(next); } else if (color[next] == color[node]) { return false; // same color on both ends: odd cycle } } } } return true; } ``` ## C# ```csharp public static bool IsBipartite(int n, List> adj) { var color = new int[n]; Array.Fill(color, -1); for (int start = 0; start < n; start++) { if (color[start] != -1) continue; color[start] = 0; var queue = new Queue(); queue.Enqueue(start); while (queue.Count > 0) { int node = queue.Dequeue(); foreach (int next in adj[node]) { if (color[next] == -1) { color[next] = 1 - color[node]; queue.Enqueue(next); } else if (color[next] == color[node]) { return false; // same color on both ends: odd cycle } } } } return true; } ``` --- title: "Greedy (exchange argument)" url: https://algopath.pro/patterns/greedy language: en summary: "Take the best-looking choice right now and never revisit it. It works only when a proof says the local choice is always safe." updated: 2026-10-17 --- # Greedy (exchange argument) Take the best-looking choice right now and never revisit it. It works only when a proof says the local choice is always safe. ## How does Greedy (exchange argument) work? Name the choice made at each step. Usually it is the smallest, largest or earliest option. Sort the input so that choice is always at the front. That sort is most of the work. Walk the list once and take whatever still fits. Never look back at a choice already made. The whole method rests on one claim. Taking the local best never rules out the global best. Prove it by exchange: take any optimal answer and swap your choice in. If it stays optimal, greedy is safe. Without that proof greedy fails quietly. It returns a plausible answer that is not the best. Sources: [Wikipedia: Greedy algorithm](https://en.wikipedia.org/wiki/Greedy_algorithm) - `sorted by end: [1,3], [2,4], [3,5]` Booking meetings. The earliest finish comes first. - `take [1, 3]` It finishes soonest, so it leaves the most room. - `skip [2, 4]` It starts at 2, before the last one ended. - `take [3, 5]` It starts exactly when the room frees up. - `answer = 2` Two meetings fit. Sorting by length instead can lose one. ## When should you use Greedy (exchange argument)? Use Greedy (exchange argument) when the problem statement says “fewest coins or pieces to make an amount”, “largest that still fits, taken greedily” or “make change with denominations”. A solution built on it usually runs in O(n log n). - fewest coins or pieces to make an amount - largest that still fits, taken greedily - make change with denominations - prove optimality with an exchange argument ## What is Greedy (exchange argument) confused with? Greedy (exchange argument) is most often confused with Dynamic programming (1-D), Backtracking, Sort with a custom comparator and Binary search on the answer. - **Dynamic programming (1-D)** - DP keeps every option open until the end. Greedy commits at once and cannot undo. - **Backtracking** - Backtracking tries every choice and keeps the best. Greedy tries one and trusts it. - **Sort with a custom comparator** - Most greedy solutions begin with a sort. The comparator is where the choice is written. - **Binary search on the answer** - There the feasibility test is often greedy itself. The search only picks the value to test. ## What is the time and space complexity of Greedy (exchange argument)? The target complexity of Greedy (exchange argument) is O(n log n). O(n log n) when the input has to be sorted first. The pass afterwards is a single loop, O(n). ## A worked example of Greedy (exchange argument) ### Fewest arrows to burst the balloons Balloons cover horizontal ranges. An arrow shot at x bursts every balloon covering x. Return the fewest arrows that burst all of them. Sort the balloons by their right edge. Shoot at the first right edge and skip every balloon it covers. Then shoot at the next uncovered edge. ```javascript function findMinArrowShots(points) { points.sort((a, b) => a[1] - b[1]); // by right edge, never by left let arrows = 1; let shot = points[0][1]; // the earliest right edge for (const [start, end] of points) { if (start > shot) { arrows++; shot = end; // this balloon is out of reach of the last arrow } } return arrows; } ``` ## Common mistakes with Greedy (exchange argument) - **Skipping the proof** A greedy rule that sounds right is very often wrong. Hunt for a counterexample first. - **Sorting by the wrong key** Earliest start and earliest end give different answers. The proof picks the key, not intuition. - **Using it where the choices interact** Coin change with odd denominations breaks greedy. There DP is the only safe answer. - **Trying to undo a choice** Once greedy takes something back, it is a search. Write it as backtracking or DP instead. ## Which interview problems use Greedy (exchange argument)? Interview problems solved with Greedy (exchange argument) include Non-overlapping intervals, Minimum number of arrows, Jump game, Gas station, Task scheduler, Partition labels and Assign cookies. - **Non-overlapping intervals** Keep the interval that finishes first. - **Minimum number of arrows** One arrow per group of overlapping ranges. - **Jump game** Track the furthest index still reachable. - **Gas station** Restart at the point where the tank went negative. - **Task scheduler** Place the most frequent task first each round. - **Partition labels** Cut as soon as every letter seen has finished. - **Assign cookies** Give each child the smallest cookie that satisfies them. ## JavaScript ```javascript function minCoinsGreedy(coins, amount) { const sorted = [...coins].sort((a, b) => b - a); const used = []; for (const coin of sorted) { while (amount >= coin) { amount -= coin; used.push(coin); } } return amount === 0 ? used : null; } ``` ## TypeScript ```typescript function minCoinsGreedy(coins: number[], amount: number): number[] | null { const sorted = [...coins].sort((a, b) => b - a); const used: number[] = []; for (const coin of sorted) { while (amount >= coin) { amount -= coin; used.push(coin); } } return amount === 0 ? used : null; } ``` ## Python ```python def min_coins_greedy(coins, amount): used = [] for coin in sorted(coins, reverse=True): while amount >= coin: amount -= coin used.append(coin) return used if amount == 0 else None ``` ## PHP ```php function minCoinsGreedy(array $coins, int $amount): ?array { rsort($coins); $used = []; foreach ($coins as $coin) { while ($amount >= $coin) { $amount -= $coin; $used[] = $coin; } } return $amount === 0 ? $used : null; } ``` ## Java ```java public static List minCoinsGreedy(int[] coins, int amount) { int[] sorted = coins.clone(); Arrays.sort(sorted); // ascending, so walk it backwards for the biggest first List used = new ArrayList<>(); for (int i = sorted.length - 1; i >= 0; i--) { int coin = sorted[i]; while (amount >= coin) { amount -= coin; used.add(coin); } } return amount == 0 ? used : null; } ``` ## C# ```csharp public static List MinCoinsGreedy(int[] coins, int amount) { var sorted = (int[]) coins.Clone(); Array.Sort(sorted); Array.Reverse(sorted); // biggest coin first var used = new List(); foreach (int coin in sorted) { while (amount >= coin) { amount -= coin; used.Add(coin); } } return amount == 0 ? used : null; } ``` --- title: "Dynamic programming (1-D)" url: https://algopath.pro/patterns/dynamic-programming language: en summary: "Answer a small version of the problem, store the result, then build the next one from that. Every state is computed once." updated: 2026-10-17 --- # Dynamic programming (1-D) Answer a small version of the problem, store the result, then build the next one from that. Every state is computed once. ## How does Dynamic programming (1-D) work? Decide what one state means. Usually it is the best answer over the first i items. Write the transition: how state i follows from earlier ones. That transition is the whole algorithm. Fill the base cases by hand. Those are the states with nothing smaller to lean on. Loop forward and fill the table in order. Every state it reads is already finished. The answer is one cell, usually the last. Sometimes it is the largest cell in the table. If the transition looks only two states back, keep two variables. The array is then unnecessary. Sources: [Wikipedia: Dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming), [Bellman (1954), The theory of dynamic programming](https://doi.org/10.1090/S0002-9904-1954-09848-8) - `dp[0] = 2` Robbing houses [2, 7, 9]. Taking only the first gives 2. - `dp[1] = 7` Take the better of 2 and 7. Neighbouring houses cannot both be robbed. - `dp[2] = max(7, 2 + 9)` Either skip the third house, or take it plus dp[0]. - `dp[2] = 11` Taking the first and third houses wins. - `answer = 11` Three states, each computed exactly once. ## When should you use Dynamic programming (1-D)? Use Dynamic programming (1-D) when the problem statement says “best/maximum/minimum ending at position i”, “ways to reach, climb, or tile” or “cannot pick two adjacent items”. A solution built on it usually runs in O(states). - best/maximum/minimum ending at position i - ways to reach, climb, or tile - cannot pick two adjacent items - minimum cost path through a grid - overlapping subproblems, same state recomputed ## What is Dynamic programming (1-D) confused with? Dynamic programming (1-D) is most often confused with Recursion with memoization, Greedy (exchange argument), Recursion and DP on subsequences (LIS / LCS / edit distance). - **Recursion with memoization** - Memoisation fills the same table from the top, on demand. This fills it upward in a fixed order. - **Greedy (exchange argument)** - Greedy commits to one choice per step. DP keeps every option until the end. - **Recursion** - The recursion tells you what the transition is. The table is what makes it fast. - **DP on subsequences (LIS / LCS / edit distance)** - There the state is a pair of positions in two sequences. Here one index is enough. ## What is the time and space complexity of Dynamic programming (1-D)? The target complexity of Dynamic programming (1-D) is O(states). n states with O(1) work each gives O(n). Memory drops to O(1) when only the last states matter. ## A worked example of Dynamic programming (1-D) ### Largest sum of a contiguous stretch Return the largest sum of any contiguous stretch of an array. Values can be negative, so the answer is not simply the total. Let state i be the best sum of a stretch that ends at index i. Either the previous stretch continues, or a new one starts here. The answer is the largest state. ```javascript function maxSubArray(nums) { let best = nums[0]; let endingHere = nums[0]; // only the previous state matters, so no array for (let i = 1; i < nums.length; i++) { // continue the stretch, or start a new one right here endingHere = Math.max(nums[i], endingHere + nums[i]); best = Math.max(best, endingHere); } return best; } ``` ## Common mistakes with Dynamic programming (1-D) - **A state that does not decide the future** If two situations share a state but behave differently, the state is incomplete. Add what is missing. - **Filling the table in the wrong order** A cell has to be written before anything reads it. Follow the direction the transition needs. - **Reading the wrong cell as the answer** For a best-ending-here state the answer is the maximum. It is not the last cell. - **Forgetting the base cases** An empty or single-element input is where most of these break. Handle those first. ## Which interview problems use Dynamic programming (1-D)? Interview problems solved with Dynamic programming (1-D) include Climbing stairs, House robber, Maximum subarray, Coin change, Decode ways, Min cost climbing stairs and Fibonacci. - **Climbing stairs** Each step comes from one or two behind. - **House robber** Take this house plus two back, or skip it. - **Maximum subarray** Continue the stretch or start a new one. - **Coin change** The state is the amount, and every coin is a branch. - **Decode ways** One digit or two, when the pair is valid. - **Min cost climbing stairs** The same shape with a cost on each step. - **Fibonacci** The shortest transition there is. ## JavaScript ```javascript function maxSubarraySum(nums) { let best = nums[0]; let curr = nums[0]; for (let i = 1; i < nums.length; i++) { curr = Math.max(nums[i], curr + nums[i]); best = Math.max(best, curr); } return best; } ``` ## TypeScript ```typescript function maxSubarraySum(nums: number[]): number { let best = nums[0]; let curr = nums[0]; for (let i = 1; i < nums.length; i++) { curr = Math.max(nums[i], curr + nums[i]); best = Math.max(best, curr); } return best; } ``` ## Python ```python def max_subarray_sum(nums): best = curr = nums[0] for x in nums[1:]: curr = max(x, curr + x) best = max(best, curr) return best ``` ## PHP ```php function maxSubarraySum(array $nums): int { $best = $curr = $nums[0]; for ($i = 1; $i < count($nums); $i++) { $curr = max($nums[$i], $curr + $nums[$i]); $best = max($best, $curr); } return $best; } ``` ## Java ```java public static int maxSubarraySum(int[] nums) { int best = nums[0]; int curr = nums[0]; for (int i = 1; i < nums.length; i++) { curr = Math.max(nums[i], curr + nums[i]); best = Math.max(best, curr); } return best; } ``` ## C# ```csharp public static int MaxSubarraySum(int[] nums) { int best = nums[0]; int curr = nums[0]; for (int i = 1; i < nums.Length; i++) { curr = Math.Max(nums[i], curr + nums[i]); best = Math.Max(best, curr); } return best; } ``` --- title: "DP on subsequences (LIS / LCS / edit distance)" url: https://algopath.pro/patterns/dp-subsequence language: en summary: "One sequence keeps dp[i], the longest chain ending at item i. Two sequences fill a table whose cell i and j answers both prefixes." updated: 2026-10-17 --- # DP on subsequences (LIS / LCS / edit distance) One sequence keeps dp[i], the longest chain ending at item i. Two sequences fill a table whose cell i and j answers both prefixes. ## How does DP on subsequences (LIS / LCS / edit distance) work? With one sequence, dp[i] is the longest increasing chain that ends at item i. Every dp[i] starts at 1, the item on its own. For each i, look back at every earlier j. If arr[j] is smaller, the chain at j can take arr[i] next. So dp[i] becomes the larger of itself and dp[j] plus one. The answer is the largest dp[i], not the last one. With two sequences, build a table instead. Cell i and j is the answer for those two prefixes. If the two current items match, add one to the cell up and left. If not, take the better of the two neighbouring cells. Row zero and column zero stand for the empty prefixes. Each row reads only the row above, so one row of memory is enough. Sources: [Wikipedia: Longest common subsequence](https://en.wikipedia.org/wiki/Longest_common_subsequence), [Wikipedia: Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance), [Wagner, Fischer (1974), The string-to-string correction problem](https://doi.org/10.1145/321796.321811) - `arr = [2, 5, 3, 7, 4, 8], every dp = 1` Each item on its own is a chain of length 1. - `dp[1] = 2, dp[2] = 2` Both 5 and 3 can follow 2. Nothing else before them is smaller. - `dp[3] = 3` 7 can follow 5 or 3, and each of those has dp 2. Add one. - `dp[4] = 3` 4 can follow 3 but not 5 or 7. So it extends 2, 3. - `dp = [1, 2, 2, 3, 3, 4], answer = 4` 8 follows any earlier item. One longest chain is 2, 5, 7, 8. ## When should you use DP on subsequences (LIS / LCS / edit distance)? Use DP on subsequences (LIS / LCS / edit distance) when the problem statement says “longest increasing subsequence”, “longest common subsequence between two strings” or “minimum edits to turn one string into another”. A solution built on it usually runs in O(n^2)/O(nm). - longest increasing subsequence - longest common subsequence between two strings - minimum edits to turn one string into another - keep relative order, may skip items - diff or alignment between two sequences ## What is DP on subsequences (LIS / LCS / edit distance) confused with? DP on subsequences (LIS / LCS / edit distance) is most often confused with Dynamic programming (1-D), DP on strings (word break), Binary search (array) and Two pointers (same direction). - **Dynamic programming (1-D)** - One index is enough there. Comparing two sequences needs two of them. - **DP on strings (word break)** - That page splits a single string into pieces. This one lines two sequences up. - **Binary search (array)** - The n log n form of increasing subsequence uses binary search. The table form is the slower one. - **Two pointers (same direction)** - Checking whether one string is a subsequence needs no table. Finding the longest common one does. ## What is the time and space complexity of DP on subsequences (LIS / LCS / edit distance)? The target complexity of DP on subsequences (LIS / LCS / edit distance) is O(n^2)/O(nm). One sequence of n items costs O(n²) time. Two sequences of n and m cost O(nm), and one kept row is enough. ## A worked example of DP on subsequences (LIS / LCS / edit distance) ### Edit distance between two words Return the fewest single-character edits that turn one word into another. An edit inserts, deletes or replaces exactly one character. Cell i and j holds the cost of turning one prefix into the other. On a match the cost comes from the diagonal. On a mismatch it is one plus the cheapest neighbour. ```javascript function minDistance(a, b) { // row 0 and column 0 hold the cost of deleting a whole prefix const dp = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)) ); for (let i = 1; i <= a.length; i++) { for (let j = 1; j <= b.length; j++) { if (a[i - 1] === b[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; // no edit needed } else { // replace, delete, insert dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]); } } } return dp[a.length][b.length]; } ``` ## Common mistakes with DP on subsequences (LIS / LCS / edit distance) - **Confusing a subsequence with a substring** A subsequence may skip items, a substring may not. The transition is not the same. - **Off by one between table and string** Cell i refers to the character at i minus one. Mixing them shifts every comparison. - **Filling the base row with zeroes** Edit distance starts with the cost of deleting a whole prefix. Zeroes there give a wrong answer. - **Using the table when n is large** O(n squared) dies at 1e5 items. Increasing subsequence has an n log n form. ## Which interview problems use DP on subsequences (LIS / LCS / edit distance)? Interview problems solved with DP on subsequences (LIS / LCS / edit distance) include Longest common subsequence, Edit distance, Longest increasing subsequence, Delete operation for two strings, Distinct subsequences, Interleaving string and Shortest common supersequence. - **Longest common subsequence** The plain form, over two sequences. - **Edit distance** Three neighbours to choose from instead of two. - **Longest increasing subsequence** One sequence, compared against itself. - **Delete operation for two strings** Total length minus twice the common subsequence. - **Distinct subsequences** Count the ways rather than the length. - **Interleaving string** Two sources feeding one target string. - **Shortest common supersequence** Rebuild the answer by walking the table backwards. ## JavaScript ```javascript function longestIncreasingSubsequence(arr) { const dp = new Array(arr.length).fill(1); let best = arr.length ? 1 : 0; for (let i = 1; i < arr.length; i++) { for (let j = 0; j < i; j++) { if (arr[j] < arr[i]) dp[i] = Math.max(dp[i], dp[j] + 1); } best = Math.max(best, dp[i]); } return best; } ``` ## TypeScript ```typescript function longestIncreasingSubsequence(arr: number[]): number { const dp = new Array(arr.length).fill(1); let best = arr.length ? 1 : 0; for (let i = 1; i < arr.length; i++) { for (let j = 0; j < i; j++) { if (arr[j] < arr[i]) dp[i] = Math.max(dp[i], dp[j] + 1); } best = Math.max(best, dp[i]); } return best; } ``` ## Python ```python def longest_increasing_subsequence(arr): if not arr: return 0 dp = [1] * len(arr) for i in range(1, len(arr)): for j in range(i): if arr[j] < arr[i]: dp[i] = max(dp[i], dp[j] + 1) return max(dp) ``` ## PHP ```php function longestIncreasingSubsequence(array $arr): int { $n = count($arr); if ($n === 0) return 0; $dp = array_fill(0, $n, 1); for ($i = 1; $i < $n; $i++) { for ($j = 0; $j < $i; $j++) { if ($arr[$j] < $arr[$i]) { $dp[$i] = max($dp[$i], $dp[$j] + 1); } } } return max($dp); } ``` ## Java ```java public static int longestIncreasingSubsequence(int[] arr) { int[] dp = new int[arr.length]; Arrays.fill(dp, 1); int best = arr.length > 0 ? 1 : 0; for (int i = 1; i < arr.length; i++) { for (int j = 0; j < i; j++) { if (arr[j] < arr[i]) dp[i] = Math.max(dp[i], dp[j] + 1); } best = Math.max(best, dp[i]); } return best; } ``` ## C# ```csharp public static int LongestIncreasingSubsequence(int[] arr) { var dp = new int[arr.Length]; Array.Fill(dp, 1); int best = arr.Length > 0 ? 1 : 0; for (int i = 1; i < arr.Length; i++) { for (int j = 0; j < i; j++) { if (arr[j] < arr[i]) dp[i] = Math.Max(dp[i], dp[j] + 1); } best = Math.Max(best, dp[i]); } return best; } ``` --- title: "DP on strings (word break)" url: https://algopath.pro/patterns/dp-strings language: en summary: "Cut the string at every position and ask whether that piece is valid. A table of reachable positions kills the repeats off." updated: 2026-10-17 --- # DP on strings (word break) Cut the string at every position and ask whether that piece is valid. A table of reachable positions kills the repeats off. ## How does DP on strings (word break) work? State i means the first i characters can be split. State zero is true by definition. For each position i, look back at every earlier position j. The piece between them is the candidate word. The split works when state j is true and that piece is a word. Then state i is true as well. One working j is enough. Stop the inner loop the moment you find it. The answer is the last state. Nothing beyond position n exists. Each position is decided exactly once. That is what turns exponential branching into a table. Sources: [Wikipedia: Dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming) - `dp[0] = true` Splitting leetcode with the words leet and code. - `dp[1] to dp[3] = false` The pieces l, le and lee are not words. - `dp[4] = true` leet is a word and dp[0] is true. - `dp[5] to dp[7] = false` No word ends at any of those positions. - `dp[8] = true` code follows dp[4]. The whole string splits. ## When should you use DP on strings (word break)? Use DP on strings (word break) when the problem statement says “can the string be split into dictionary words”, “segment a string using a word list” or “boolean reachable at each cut position”. A solution built on it usually runs in O(n^2). - can the string be split into dictionary words - segment a string using a word list - boolean reachable at each cut position - glued text needs word boundaries ## What is DP on strings (word break) confused with? DP on strings (word break) is most often confused with DP on subsequences (LIS / LCS / edit distance), Trie (prefix tree), Backtracking and Hash set / map. - **DP on subsequences (LIS / LCS / edit distance)** - That lines up two separate sequences. This one splits a single string. - **Trie (prefix tree)** - A trie makes the dictionary lookup fast. The splitting logic above it stays the same. - **Backtracking** - Backtracking lists every valid split. The table only answers whether one exists. - **Hash set / map** - The dictionary is usually a set. The table is what stops a suffix being retried. ## What is the time and space complexity of DP on strings (word break)? The target complexity of DP on strings (word break) is O(n^2). n characters with words up to m give O(nm). A set or a trie keeps the lookup near O(1). ## A worked example of DP on strings (word break) ### Fewest cuts into palindromes Cut a string into pieces that are all palindromes. Return the fewest cuts needed. A single character already counts as a palindrome. First mark every pair of positions that spans a palindrome. Then state i is the fewest cuts for the first i characters. A palindromic tail costs one cut more. ```javascript function minCut(s) { const n = s.length; const isPal = Array.from({ length: n }, () => new Array(n).fill(false)); for (let end = 0; end < n; end++) { for (let start = end; start >= 0; start--) { // the inside is already decided, because end - start only shrinks if (s[start] === s[end] && (end - start < 2 || isPal[start + 1][end - 1])) { isPal[start][end] = true; } } } const cuts = new Array(n + 1).fill(0); for (let i = 0; i <= n; i++) cuts[i] = i - 1; // cuts[0] is -1, so a whole palindrome costs 0 for (let end = 0; end < n; end++) { for (let start = 0; start <= end; start++) { if (isPal[start][end]) { cuts[end + 1] = Math.min(cuts[end + 1], cuts[start] + 1); } } } return cuts[n]; } ``` ## Common mistakes with DP on strings (word break) - **Rebuilding the substring every step** Slicing inside the loop copies characters each time. Compare positions, or walk a trie. - **Looping the wrong way for palindromes** The inside has to be decided before the outside. Grow by length, or walk the start backwards. - **Forgetting the empty prefix** State zero is what lets the first word begin a split. Without it nothing is ever true. - **Listing every split when one is enough** Word break asks only yes or no. Enumerating the splits is exponential. ## Which interview problems use DP on strings (word break)? Interview problems solved with DP on strings (word break) include Word break, Word break II, Palindrome partitioning II, Longest palindromic substring, Concatenated words, Extra characters in a string and Decode ways. - **Word break** Whether the string can be split at all. - **Word break II** Every valid split, so backtracking comes back. - **Palindrome partitioning II** The fewest cuts, not the partitions themselves. - **Longest palindromic substring** The same palindrome table, read differently. - **Concatenated words** Each word is split using all of the others. - **Extra characters in a string** The cost of whatever letters are left over. - **Decode ways** One digit or two, over a fixed alphabet. ## JavaScript ```javascript function wordBreak(s, wordDict) { const words = new Set(wordDict); const ok = new Array(s.length + 1).fill(false); ok[0] = true; for (let i = 1; i <= s.length; i++) { for (let j = 0; j < i; j++) { if (ok[j] && words.has(s.slice(j, i))) { ok[i] = true; break; } } } return ok[s.length]; } ``` ## TypeScript ```typescript function wordBreak(s: string, wordDict: string[]): boolean { const words = new Set(wordDict); const ok = new Array(s.length + 1).fill(false); ok[0] = true; for (let i = 1; i <= s.length; i++) { for (let j = 0; j < i; j++) { if (ok[j] && words.has(s.slice(j, i))) { ok[i] = true; break; } } } return ok[s.length]; } ``` ## Python ```python def word_break(s, word_dict): words = set(word_dict) ok = [False] * (len(s) + 1) ok[0] = True for i in range(1, len(s) + 1): for j in range(i): if ok[j] and s[j:i] in words: ok[i] = True break return ok[len(s)] ``` ## PHP ```php function wordBreak(string $s, array $wordDict): bool { $words = array_flip($wordDict); $n = strlen($s); $ok = array_fill(0, $n + 1, false); $ok[0] = true; for ($i = 1; $i <= $n; $i++) { for ($j = 0; $j < $i; $j++) { if ($ok[$j] && isset($words[substr($s, $j, $i - $j)])) { $ok[$i] = true; break; } } } return $ok[$n]; } ``` ## Java ```java public static boolean wordBreak(String s, String[] wordDict) { Set words = new HashSet<>(Arrays.asList(wordDict)); boolean[] ok = new boolean[s.length() + 1]; ok[0] = true; for (int i = 1; i <= s.length(); i++) { for (int j = 0; j < i; j++) { if (ok[j] && words.contains(s.substring(j, i))) { ok[i] = true; break; } } } return ok[s.length()]; } ``` ## C# ```csharp public static bool WordBreak(string s, string[] wordDict) { var words = new HashSet(wordDict); var ok = new bool[s.Length + 1]; ok[0] = true; for (int i = 1; i <= s.Length; i++) { for (int j = 0; j < i; j++) { if (ok[j] && words.Contains(s.Substring(j, i - j))) { ok[i] = true; break; } } } return ok[s.Length]; } ``` --- title: "DP on intervals" url: https://algopath.pro/patterns/dp-interval language: en summary: "The state is a range, and the transition picks the last move made inside it. The shorter ranges are always filled first." updated: 2026-10-17 --- # DP on intervals The state is a range, and the transition picks the last move made inside it. The shorter ranges are always filled first. ## How does DP on intervals work? The state is a pair, the two ends of a range. It holds the best result inside those bounds. Loop over the length of the range, from short to long. Every range you read is already finished. For one range, try every split point inside it. Each split gives one candidate answer. A candidate combines the left part, the right part and the cost of joining them. Keep the best of them. The joining cost is where the problem actually lives. In burst balloons it depends on the two boundaries. The answer is the state covering the whole input. It is the very last range filled. Sources: [Wikipedia: Matrix chain multiplication](https://en.wikipedia.org/wiki/Matrix_chain_multiplication) - `padded: [1, 3, 1, 5, 1]` Bursting balloons. A one is added at each end and never bursts. - `width one ranges: 0` There is nothing inside them to burst. - `dp(1, 3) = 15` Bursting the 1 between 3 and 5 pays 3 times 1 times 5. - `dp(0, 3) = 30` Burst the 3 last, so it is worth 1 times 3 times 5. - `dp(0, 4) = 35` The 5 goes last. Every shorter range was already done. ## When should you use DP on intervals? Use DP on intervals when the problem statement says “combine adjacent pieces at some cost”, “best way to split a range into two parts” or “matrix chain multiplication order”. A solution built on it usually runs in O(n^3). - combine adjacent pieces at some cost - best way to split a range into two parts - matrix chain multiplication order - merge cost depends on the whole span - optimal parenthesization or build order ## What is DP on intervals confused with? DP on intervals is most often confused with Dynamic programming (1-D), DP on subsequences (LIS / LCS / edit distance), DP on trees and Greedy (exchange argument). - **Dynamic programming (1-D)** - One index describes a prefix. A range needs both of its ends. - **DP on subsequences (LIS / LCS / edit distance)** - There the two indices point into two different sequences. Here both sit in one. - **DP on trees** - A tree splits at its children, which are given. An interval splits at a point you choose. - **Greedy (exchange argument)** - Greedy would pick an order and commit to it. Here every split point has to be tried. ## What is the time and space complexity of DP on intervals? The target complexity of DP on intervals is O(n^3). n up to about 500 gives O(n cubed). There are n squared ranges, each with n split points. ## A worked example of DP on intervals ### Longest palindromic subsequence Return the length of the longest palindromic subsequence in a string. The letters do not have to sit next to each other. The state is a range of the string. If the two ends match, they add two to the range inside. If not, drop one end and take the better side. ```javascript function longestPalindromeSubseq(s) { const n = s.length; const dp = Array.from({ length: n }, () => new Array(n).fill(0)); // i walks backwards so every inner range is already filled for (let i = n - 1; i >= 0; i--) { dp[i][i] = 1; // a single letter is a palindrome for (let j = i + 1; j < n; j++) { if (s[i] === s[j]) { dp[i][j] = dp[i + 1][j - 1] + 2; } else { dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); } } } return dp[0][n - 1]; } ``` ## Common mistakes with DP on intervals - **Looping i and j like a normal grid** A range must be filled after everything inside it. Loop by length, or walk i backwards. - **Choosing the first move instead of the last** In burst balloons only the last burst has fixed neighbours. Picking the first makes the state wrong. - **Forgetting the padding** A neutral value at each end removes the edge cases. Without it the boundaries need special code. - **Running it on a large input** O(n cubed) dies past a few thousand items. Check n before writing three loops. ## Which interview problems use DP on intervals? Interview problems solved with DP on intervals include Burst balloons, Longest palindromic subsequence, Minimum score triangulation, Strange printer, Remove boxes, Count palindromic substrings and Minimum cost to merge stones. - **Burst balloons** Choose which balloon is burst last. - **Longest palindromic subsequence** The two ends match, or one is dropped. - **Minimum score triangulation** Every split point forms a triangle. - **Strange printer** One print covers a whole range. - **Remove boxes** The state carries an extra count alongside the range. - **Count palindromic substrings** Count every range that is a palindrome. - **Minimum cost to merge stones** Split into groups of k rather than two. ## JavaScript ```javascript function minMergeCost(sizes) { const n = sizes.length; const prefix = [0]; for (const s of sizes) prefix.push(prefix[prefix.length - 1] + s); const dp = Array.from({ length: n }, () => new Array(n).fill(0)); for (let len = 2; len <= n; len++) { for (let i = 0; i + len - 1 < n; i++) { const j = i + len - 1; dp[i][j] = Infinity; for (let k = i; k < j; k++) { const cost = dp[i][k] + dp[k + 1][j] + (prefix[j + 1] - prefix[i]); dp[i][j] = Math.min(dp[i][j], cost); } } } return dp[0][n - 1]; } ``` ## TypeScript ```typescript function minMergeCost(sizes: number[]): number { const n = sizes.length; const prefix: number[] = [0]; for (const s of sizes) prefix.push(prefix[prefix.length - 1] + s); const dp: number[][] = Array.from({ length: n }, () => new Array(n).fill(0)); for (let len = 2; len <= n; len++) { for (let i = 0; i + len - 1 < n; i++) { const j = i + len - 1; dp[i][j] = Infinity; for (let k = i; k < j; k++) { const cost = dp[i][k] + dp[k + 1][j] + (prefix[j + 1] - prefix[i]); dp[i][j] = Math.min(dp[i][j], cost); } } } return dp[0][n - 1]; } ``` ## Python ```python def min_merge_cost(sizes): n = len(sizes) prefix = [0] for s in sizes: prefix.append(prefix[-1] + s) dp = [[0] * n for _ in range(n)] for length in range(2, n + 1): for i in range(n - length + 1): j = i + length - 1 dp[i][j] = min( dp[i][k] + dp[k + 1][j] + (prefix[j + 1] - prefix[i]) for k in range(i, j) ) return dp[0][n - 1] ``` ## PHP ```php function minMergeCost(array $sizes): int { $n = count($sizes); $prefix = [0]; foreach ($sizes as $s) $prefix[] = end($prefix) + $s; $dp = array_fill(0, $n, array_fill(0, $n, 0)); for ($len = 2; $len <= $n; $len++) { for ($i = 0; $i + $len - 1 < $n; $i++) { $j = $i + $len - 1; $dp[$i][$j] = PHP_INT_MAX; for ($k = $i; $k < $j; $k++) { $cost = $dp[$i][$k] + $dp[$k + 1][$j] + ($prefix[$j + 1] - $prefix[$i]); $dp[$i][$j] = min($dp[$i][$j], $cost); } } } return $dp[0][$n - 1]; } ``` ## Java ```java public static int minMergeCost(int[] sizes) { int n = sizes.length; int[] prefix = new int[n + 1]; for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + sizes[i]; int[][] dp = new int[n][n]; for (int len = 2; len <= n; len++) { for (int i = 0; i + len - 1 < n; i++) { int j = i + len - 1; dp[i][j] = Integer.MAX_VALUE; for (int k = i; k < j; k++) { int cost = dp[i][k] + dp[k + 1][j] + (prefix[j + 1] - prefix[i]); dp[i][j] = Math.min(dp[i][j], cost); } } } return dp[0][n - 1]; } ``` ## C# ```csharp public static int MinMergeCost(int[] sizes) { int n = sizes.Length; var prefix = new int[n + 1]; for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + sizes[i]; var dp = new int[n][]; for (int i = 0; i < n; i++) dp[i] = new int[n]; for (int len = 2; len <= n; len++) { for (int i = 0; i + len - 1 < n; i++) { int j = i + len - 1; dp[i][j] = int.MaxValue; for (int k = i; k < j; k++) { int cost = dp[i][k] + dp[k + 1][j] + (prefix[j + 1] - prefix[i]); dp[i][j] = Math.Min(dp[i][j], cost); } } } return dp[0][n - 1]; } ``` --- title: "DP on trees" url: https://algopath.pro/patterns/dp-on-trees language: en summary: "Every node's answer is built out of the answers given by its own children. A single post-order pass computes all of them." updated: 2026-10-17 --- # DP on trees Every node's answer is built out of the answers given by its own children. A single post-order pass computes all of them. ## How does DP on trees work? Decide what one node returns to its parent. Often it is a pair, one value per choice. Recurse into every child first. Nothing can be decided before they answer. Combine the children's answers into this node's answer. That combination is the transition. Return it upward. The parent knows nothing else about the subtree below. Record a global best while unwinding, if the answer can sit anywhere. The root alone is not always enough. One post-order pass touches each node once. That is the whole cost. Sources: [Wikipedia: Dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming), [Wikipedia: Tree traversal](https://en.wikipedia.org/wiki/Tree_traversal) - `leaf returns (3, 0)` Robbing houses on a tree. Robbing the leaf gives 3, skipping gives 0. - `node 2 returns (2, 3)` Rob it and skip the child, or skip it and take the better. - `node 3 returns (3, 1)` Its own child is worth only 1. - `root robbed = 3 + 3 + 1` Robbing the root forces both children to be skipped. - `answer = 7` Seven beats the six that skipping the root would give. ## When should you use DP on trees? Use DP on trees when the problem statement says “best choice over a hierarchy or org chart”, “cannot pick a node and its direct parent together” or “post-order: children settled before the parent”. A solution built on it usually runs in O(n). - best choice over a hierarchy or org chart - cannot pick a node and its direct parent together - post-order: children settled before the parent - each node combines states from its children ## What is DP on trees confused with? DP on trees is most often confused with Tree traversal, Dynamic programming (1-D), Recursion with memoization and Graph BFS / DFS. - **Tree traversal** - That page is about the order of visiting. This one is about what gets carried back up. - **Dynamic programming (1-D)** - A line has one direction, so an index is enough. Here the tree structure sets the order. - **Recursion with memoization** - A tree has no repeated subproblems, so no cache is needed. Each node is visited once. - **Graph BFS / DFS** - A general graph can reach a node twice, so the recursion may not end. A tree cannot. ## What is the time and space complexity of DP on trees? The target complexity of DP on trees is O(n). n nodes give O(n) when each node does O(1) work. Several states per node multiply that. ## A worked example of DP on trees ### Largest sum along any path Return the largest sum along any path in a binary tree. A path may start and end anywhere, but it may not fork twice. Each node returns the best straight path running down through it. A negative branch is dropped. The best forking path is recorded globally while the calls unwind. ```javascript function maxPathSum(root) { let best = -Infinity; function down(node) { if (!node) return 0; // a negative branch is worse than taking nothing at all const left = Math.max(0, down(node.left)); const right = Math.max(0, down(node.right)); best = Math.max(best, node.val + left + right); // the fork stays local return node.val + Math.max(left, right); // the parent can only use one side } down(root); return best; } ``` ## Common mistakes with DP on trees - **Returning the same value you record** The value carried up is a straight path, but the best answer may fork. Keep the two apart. - **Computing before the children answer** The transition needs both children first. Recurse first, then combine. - **Missing the empty-child case** A node with one child still calls the other side. Return the neutral value there. - **Starting the best at zero** Starting at zero hides a tree whose values are all negative. Start at minus infinity instead. ## Which interview problems use DP on trees? Interview problems solved with DP on trees include House robber III, Binary tree maximum path sum, Diameter of a binary tree, Longest univalue path, Distribute coins in a binary tree, Count good nodes and Binary tree tilt. - **House robber III** Two states per node: robbed or skipped. - **Binary tree maximum path sum** Return a straight path, record the fork. - **Diameter of a binary tree** Left depth plus right depth at each node. - **Longest univalue path** Extend only through children of equal value. - **Distribute coins in a binary tree** Push the surplus up to the parent. - **Count good nodes** Carry the maximum seen downward instead of up. - **Binary tree tilt** The difference between the two subtree sums. ## JavaScript ```javascript function maxNonAdjacent(node) { if (!node) return [0, 0]; // [incl, excl] let inclSum = node.value; let exclSum = 0; for (const child of node.children) { const [inclC, exclC] = maxNonAdjacent(child); inclSum += exclC; exclSum += Math.max(inclC, exclC); } return [inclSum, exclSum]; } ``` ## TypeScript ```typescript interface TreeNode { value: number; children: TreeNode[]; } function maxNonAdjacent(node: TreeNode | null): [number, number] { if (!node) return [0, 0]; // [incl, excl] let inclSum = node.value; let exclSum = 0; for (const child of node.children) { const [inclC, exclC] = maxNonAdjacent(child); inclSum += exclC; exclSum += Math.max(inclC, exclC); } return [inclSum, exclSum]; } ``` ## Python ```python def max_non_adjacent(node): if node is None: return (0, 0) # (incl, excl) incl_sum = node.value excl_sum = 0 for child in node.children: incl_c, excl_c = max_non_adjacent(child) incl_sum += excl_c excl_sum += max(incl_c, excl_c) return (incl_sum, excl_sum) ``` ## PHP ```php function maxNonAdjacent(?Node $node): array { if ($node === null) return [0, 0]; // [incl, excl] $inclSum = $node->value; $exclSum = 0; foreach ($node->children as $child) { [$inclC, $exclC] = maxNonAdjacent($child); $inclSum += $exclC; $exclSum += max($inclC, $exclC); } return [$inclSum, $exclSum]; } ``` ## Java ```java // returns {incl, excl}: the best sum that takes this node, and the best that skips it public static int[] maxNonAdjacent(TreeNode node) { if (node == null) return new int[] { 0, 0 }; int inclSum = node.value; int exclSum = 0; for (TreeNode child : node.children) { int[] best = maxNonAdjacent(child); inclSum += best[1]; exclSum += Math.max(best[0], best[1]); } return new int[] { inclSum, exclSum }; } public static class TreeNode { public int value; public List children = new ArrayList<>(); } ``` ## C# ```csharp // returns {incl, excl}: the best sum that takes this node, and the best that skips it public static int[] MaxNonAdjacent(TreeNode node) { if (node == null) return new int[] { 0, 0 }; int inclSum = node.value; int exclSum = 0; foreach (var child in node.children) { int[] best = MaxNonAdjacent(child); inclSum += best[1]; exclSum += Math.Max(best[0], best[1]); } return new int[] { inclSum, exclSum }; } public class TreeNode { public int value; public List children = new List(); } ``` --- title: "Trie (prefix tree)" url: https://algopath.pro/patterns/trie language: en summary: "Words that share a prefix also share exactly the same path down from the root. A lookup costs only the length of that word." updated: 2026-10-17 --- # Trie (prefix tree) Words that share a prefix also share exactly the same path down from the root. A lookup costs only the length of that word. ## How does Trie (prefix tree) work? The root stands for the empty prefix. It carries no character of its own. Each edge holds one character. A path from the root spells out a prefix. Inserting walks the word character by character. A missing child is created along the way. The last node of a word gets a flag. Without it, a prefix would look like a stored word. Looking up follows the same path down. A missing child means the word is not there. A prefix query stops early and reports success. Everything below that node starts with the prefix. Sources: [Wikipedia: Trie](https://en.wikipedia.org/wiki/Trie), [Fredkin (1960), Trie memory](https://doi.org/10.1145/367390.367400) - `root to c` Inserting car and cat. The first character creates one child. - `c, a, r with a flag` Three nodes and one end flag spell out car. - `insert cat: c and a exist` The shared prefix is walked, never rebuilt. - `a to t with a flag` Only one new node is added for the second word. - `look up ca: no flag` The node exists but is not a word. It is only a prefix. ## When should you use Trie (prefix tree)? Use Trie (prefix tree) when the problem statement says “autocomplete or typeahead over many strings”, “longest common prefix among words” or “insert, search, and starts-with on a word set”. A solution built on it usually runs in O(len) per op. - autocomplete or typeahead over many strings - longest common prefix among words - insert, search, and starts-with on a word set - match against a large fixed dictionary by prefix ## What is Trie (prefix tree) confused with? Trie (prefix tree) is most often confused with Hash set / map, Binary search tree, Matrix word search (grid DFS/backtracking) and DP on strings (word break). - **Hash set / map** - A map matches whole keys and nothing else. A trie also answers questions by prefix. - **Binary search tree** - A search tree compares whole keys to choose a side. A trie walks one character at a time. - **Matrix word search (grid DFS/backtracking)** - Searching a grid for many words uses a trie to prune. That page is the grid walk itself. - **DP on strings (word break)** - Word break uses a trie to test each piece. The splitting decision lives in the table. ## What is the time and space complexity of Trie (prefix tree)? The target complexity of Trie (prefix tree) is O(len) per op. A word of length m costs O(m) to insert or find. Memory is O(total characters) over every word. ## A worked example of Trie (prefix tree) ### Replace words by their roots You get a dictionary of roots and a sentence. Replace each word by the shortest root that starts it. A word with no matching root is left alone. Insert every root into a trie first. Then walk each word down the trie. Stop at the first end flag, because that is the shortest root. ```javascript function replaceWords(dictionary, sentence) { const root = { children: {} }; for (const word of dictionary) { let node = root; for (const c of word) { node.children[c] = node.children[c] ?? { children: {} }; node = node.children[c]; } node.end = true; // this node closes a real word } return sentence .split(" ") .map((word) => { let node = root; let prefix = ""; for (const c of word) { if (!node.children[c]) return word; // no root matches node = node.children[c]; prefix += c; if (node.end) return prefix; // the first flag is the shortest root } return word; }) .join(" "); } ``` ## Common mistakes with Trie (prefix tree) - **Leaving out the end flag** A stored prefix then cannot be told from a stored word. Every lookup becomes a prefix query. - **Mixing the flag in with the children** A flag stored under a letter-like key corrupts the tree. Keep children in their own map. - **Building one for a handful of words** A trie costs memory for every character. Below a few thousand words a set is simpler. - **Assuming a fixed alphabet** An array of 26 slots breaks on digits or accents. Use a map when the input is not plain letters. ## Which interview problems use Trie (prefix tree)? Interview problems solved with Trie (prefix tree) include Implement trie, Replace words, Design add and search words, Word search II, Longest common prefix, Maximum XOR of two numbers and Autocomplete system. - **Implement trie** Insert, search and the prefix query. - **Replace words** Stop the walk at the first end flag. - **Design add and search words** A dot means try every child at that level. - **Word search II** The trie prunes the grid walk early. - **Longest common prefix** Walk down while there is exactly one child. - **Maximum XOR of two numbers** A binary trie built over the bits. - **Autocomplete system** Rank the words stored under a prefix node. ## JavaScript ```javascript class TrieNode { constructor() { this.children = new Map(); this.isEnd = false; } } class Trie { constructor() { this.root = new TrieNode(); } insert(word) { let node = this.root; for (const c of word) { if (!node.children.has(c)) node.children.set(c, new TrieNode()); node = node.children.get(c); } node.isEnd = true; } search(word) { let node = this.root; for (const c of word) { if (!node.children.has(c)) return false; node = node.children.get(c); } return node.isEnd; } } ``` ## TypeScript ```typescript class TrieNode { children = new Map(); isEnd = false; } class Trie { private root = new TrieNode(); insert(word: string): void { let node = this.root; for (const c of word) { let next = node.children.get(c); if (!next) { next = new TrieNode(); node.children.set(c, next); } node = next; } node.isEnd = true; } search(word: string): boolean { let node = this.root; for (const c of word) { const next = node.children.get(c); if (!next) return false; node = next; } return node.isEnd; } } ``` ## Python ```python class TrieNode: def __init__(self): self.children = {} self.is_end = False class Trie: def __init__(self): self.root = TrieNode() def insert(self, word): node = self.root for c in word: node = node.children.setdefault(c, TrieNode()) node.is_end = True def search(self, word): node = self.root for c in word: if c not in node.children: return False node = node.children[c] return node.is_end ``` ## PHP ```php class TrieNode { public array $children = []; public bool $isEnd = false; } class Trie { private TrieNode $root; public function __construct() { $this->root = new TrieNode(); } public function insert(string $word): void { $node = $this->root; foreach (str_split($word) as $c) { $node->children[$c] ??= new TrieNode(); $node = $node->children[$c]; } $node->isEnd = true; } public function search(string $word): bool { $node = $this->root; foreach (str_split($word) as $c) { if (!isset($node->children[$c])) return false; $node = $node->children[$c]; } return $node->isEnd; } } ``` ## Java ```java public static class TrieNode { public Map children = new HashMap<>(); public boolean isEnd = false; } public static class Trie { private final TrieNode root = new TrieNode(); public void insert(String word) { TrieNode node = root; for (char c : word.toCharArray()) { node = node.children.computeIfAbsent(c, k -> new TrieNode()); } node.isEnd = true; } public boolean search(String word) { TrieNode node = root; for (char c : word.toCharArray()) { node = node.children.get(c); if (node == null) return false; } return node.isEnd; } } ``` ## C# ```csharp public class TrieNode { public Dictionary children = new Dictionary(); public bool isEnd = false; } public class Trie { private readonly TrieNode root = new TrieNode(); public void Insert(string word) { var node = root; foreach (char c in word) { if (!node.children.ContainsKey(c)) node.children[c] = new TrieNode(); node = node.children[c]; } node.isEnd = true; } public bool Search(string word) { var node = root; foreach (char c in word) { if (!node.children.TryGetValue(c, out var next)) return false; node = next; } return node.isEnd; } } ``` --- title: "Bit manipulation" url: https://algopath.pro/patterns/bit-manipulation language: en summary: "Treat a whole number as a row of switches. Masks, shifts and XOR let you read or flip any one of those switches directly." updated: 2026-10-17 --- # Bit manipulation Treat a whole number as a row of switches. Masks, shifts and XOR let you read or flip any one of those switches directly. ## How does Bit manipulation work? A number is a row of bits, each worth twice the one to its right. Bit i is worth 2 to the power i. Shifting left by one doubles the number. Shifting right by one halves it and drops the last bit. AND with a mask keeps only the bits the mask holds. That is how you read a single bit. OR sets a bit and XOR flips it. A bit XORed with itself always becomes zero. That last fact is the whole trick behind finding a lone value. Every pair cancels out. n AND n minus one clears the lowest set bit. Repeating that counts the set bits. Sources: [Wikipedia: Bitwise operation](https://en.wikipedia.org/wiki/Bitwise_operation), [MDN: Bitwise AND (&)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_AND) - `n = 1100` Twelve written in binary. Two of its bits are set. - `n & 1 = 0` The last bit is zero, so the number is even. - `n >> 2 = 11` Shifting right twice leaves three. - `n & (n - 1) = 1000` Subtracting one gives 1011, and the AND clears the lowest set bit. - `two rounds reach zero` So twelve has exactly two bits set. ## When should you use Bit manipulation? Use Bit manipulation when the problem statement says “find the single or unique number using XOR”, “set, clear, toggle, or test a bit” or “pack many yes/no flags into one integer”. A solution built on it usually runs in O(1)/O(n). - find the single or unique number using XOR - set, clear, toggle, or test a bit - pack many yes/no flags into one integer - count set bits or check a power of two ## What is Bit manipulation confused with? Bit manipulation is most often confused with Hash set / map, Math and number theory (GCD, sieve, modular), Backtracking and Non-comparison sort (counting / radix). - **Hash set / map** - A set answers membership for any key at all. A bitmask does it for 32 known items, with no memory. - **Math and number theory (GCD, sieve, modular)** - Both work on the number itself. That page is about divisors and primes, this one about bits. - **Backtracking** - Subsets can be listed by counting from zero upward. Backtracking builds the same list recursively. - **Non-comparison sort (counting / radix)** - Radix sort reads digits, sometimes as bits. Its goal is grouping, never flipping. ## What is the time and space complexity of Bit manipulation? The target complexity of Bit manipulation is O(1)/O(n). One operation is O(1) on a 32-bit value. Looping the bits is O(32), which counts as constant. ## A worked example of Bit manipulation ### Count the set bits of every number For every number from 0 to n, count how many of its bits are set. Counting each one separately works, but it repeats a lot of work. Clearing the lowest set bit gives a smaller number, already counted. So bits[i] is one more than bits[i AND i minus one]. ```javascript function countBits(n) { const bits = new Array(n + 1).fill(0); for (let i = 1; i <= n; i++) { // i & (i - 1) clears the lowest set bit, so it is always smaller bits[i] = bits[i & (i - 1)] + 1; } return bits; } ``` ## Common mistakes with Bit manipulation - **Shifting past 31 bits** JavaScript bit operators work on 32 bits and wrap around. Use BigInt beyond that. - **Forgetting the sign bit** The result of a shift can come back negative. Use the unsigned shift for a plain count. - **Mixing up AND and OR** AND reads or clears, while OR sets. Getting them backwards gives a silently wrong mask. - **Reaching for bits when clarity matters** A boolean array reads better and runs just as fast. Use a mask when memory is the constraint. ## Which interview problems use Bit manipulation? Interview problems solved with Bit manipulation include Single number, Number of 1 bits, Counting bits, Missing number, Subsets, Power of two and Sum of two integers. - **Single number** Every pair cancels itself out under XOR. - **Number of 1 bits** Clear the lowest set bit until nothing is left. - **Counting bits** Each answer reuses a smaller one already computed. - **Missing number** XOR the indices against the values. - **Subsets** Count from zero to two to the power n. - **Power of two** True exactly when n AND n minus one is zero. - **Sum of two integers** XOR gives the sum, AND shifted gives the carry. ## JavaScript ```javascript function findUnique(nums) { return nums.reduce((acc, n) => acc ^ n, 0); } function setBit(mask, i) { return mask | (1 << i); } function clearBit(mask, i) { return mask & ~(1 << i); } function hasBit(mask, i) { return (mask & (1 << i)) !== 0; } ``` ## TypeScript ```typescript function findUnique(nums: number[]): number { return nums.reduce((acc, n) => acc ^ n, 0); } function setBit(mask: number, i: number): number { return mask | (1 << i); } function clearBit(mask: number, i: number): number { return mask & ~(1 << i); } function hasBit(mask: number, i: number): boolean { return (mask & (1 << i)) !== 0; } ``` ## Python ```python def find_unique(nums): acc = 0 for n in nums: acc ^= n return acc def set_bit(mask, i): return mask | (1 << i) def clear_bit(mask, i): return mask & ~(1 << i) def has_bit(mask, i): return (mask & (1 << i)) != 0 ``` ## PHP ```php function findUnique(array $nums): int { $acc = 0; foreach ($nums as $n) $acc ^= $n; return $acc; } function setBit(int $mask, int $i): int { return $mask | (1 << $i); } function clearBit(int $mask, int $i): int { return $mask & ~(1 << $i); } function hasBit(int $mask, int $i): bool { return ($mask & (1 << $i)) !== 0; } ``` ## Java ```java public static int findUnique(int[] nums) { int acc = 0; for (int n : nums) acc ^= n; return acc; } public static int setBit(int mask, int i) { return mask | (1 << i); } public static int clearBit(int mask, int i) { return mask & ~(1 << i); } public static boolean hasBit(int mask, int i) { return (mask & (1 << i)) != 0; } ``` ## C# ```csharp public static int FindUnique(int[] nums) { int acc = 0; foreach (int n in nums) acc ^= n; return acc; } public static int SetBit(int mask, int i) { return mask | (1 << i); } public static int ClearBit(int mask, int i) { return mask & ~(1 << i); } public static bool HasBit(int mask, int i) { return (mask & (1 << i)) != 0; } ``` --- title: "Math and number theory (GCD, sieve, modular)" url: https://algopath.pro/patterns/math-number-theory language: en summary: "Number theory turns a loop over every single value into plain arithmetic. GCD, modular rules and a sieve replace brute force." updated: 2026-10-17 --- # Math and number theory (GCD, sieve, modular) Number theory turns a loop over every single value into plain arithmetic. GCD, modular rules and a sieve replace brute force. ## How does Math and number theory (GCD, sieve, modular) work? Euclid's rule replaces a pair of numbers with a smaller pair. The GCD of a and b equals that of b and a modulo b. Euclid's loop stops when the second number reaches zero. The other number is then the answer. The least common multiple follows from the GCD. It is a times b divided by their GCD. A sieve marks the multiples of each prime it meets. Whatever stays unmarked is prime. Modular arithmetic keeps the numbers small. Take the remainder after every step, not only at the end. Division has no simple modular form. It needs a modular inverse instead. Sources: [Wikipedia: Euclidean algorithm](https://en.wikipedia.org/wiki/Euclidean_algorithm), [Wikipedia: Sieve of Eratosthenes](https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes) - `gcd(48, 18)` 48 modulo 18 leaves 12. - `gcd(18, 12)` 18 modulo 12 leaves 6. - `gcd(12, 6)` 12 modulo 6 leaves nothing. - `gcd(6, 0) = 6` The second number hit zero. The answer is 6. - `lcm = 48 times 18 over 6` That gives 144, without listing a single multiple. ## When should you use Math and number theory (GCD, sieve, modular)? Use Math and number theory (GCD, sieve, modular) when the problem statement says “reduce a fraction to lowest terms”, “list all primes up to n” or “answer modulo 1e9+7”. - reduce a fraction to lowest terms - list all primes up to n - answer modulo 1e9+7 - when do two repeating intervals line up again (LCM) ## What is Math and number theory (GCD, sieve, modular) confused with? Math and number theory (GCD, sieve, modular) is most often confused with Bit manipulation, Hash set / map, Dynamic programming (1-D) and Prefix sums. - **Bit manipulation** - Both work on the number itself. That page is about the individual bits. - **Hash set / map** - A set of primes still has to be built somehow. A sieve is how you build it. - **Dynamic programming (1-D)** - A formula answers at once where DP fills a table. Look for a closed form first. - **Prefix sums** - Prefix sums answer over values that were stored. Here the answer comes from the numbers themselves. ## What is the time and space complexity of Math and number theory (GCD, sieve, modular)? GCD costs O(log of the smaller value). A sieve up to 1e7 is fine, but 1e9 is not. ## A worked example of Math and number theory (GCD, sieve, modular) ### Count the primes below n Count the prime numbers strictly below n. Testing each number on its own is far too slow once n is large. Mark every multiple of each prime as composite. Start marking at the prime squared. Everything smaller already carries a smaller factor. ```javascript function countPrimes(n) { if (n < 3) return 0; const composite = new Array(n).fill(false); let count = 0; for (let p = 2; p < n; p++) { if (composite[p]) continue; count++; // start at p * p: everything smaller already has a smaller factor for (let multiple = p * p; multiple < n; multiple += p) { composite[multiple] = true; } } return count; } ``` ## Common mistakes with Math and number theory (GCD, sieve, modular) - **Taking the remainder only at the end** The value overflows long before you get there. Reduce it after every multiplication. - **Getting a negative remainder** In JavaScript minus one modulo five is minus one. Add the modulus and take the remainder again. - **Starting the sieve's inner loop at twice the prime** Everything below the prime squared already has a smaller factor. Starting there wastes most of the work. - **Testing divisors past the square root** A factor above the root always pairs with one below it. Stop at the root. ## Which interview problems use Math and number theory (GCD, sieve, modular)? Interview problems solved with Math and number theory (GCD, sieve, modular) include Greatest common divisor of strings, Count primes, Ugly number II, Power of three, Fraction to recurring decimal, Excel sheet column number and Happy number. - **Greatest common divisor of strings** The answer's length is the GCD of the two lengths. - **Count primes** A sieve rather than a test per number. - **Ugly number II** Three pointers over the multiples of 2, 3 and 5. - **Power of three** Repeated division, or a single divisibility test. - **Fraction to recurring decimal** A repeated remainder is what marks the cycle. - **Excel sheet column number** Base 26, but with no zero digit. - **Happy number** Cycle detection over sums of squared digits. ## JavaScript ```javascript function gcd(a, b) { while (b !== 0) { [a, b] = [b, a % b]; } return a; } function lcm(a, b) { return (a / gcd(a, b)) * b; } ``` ## TypeScript ```typescript function gcd(a: number, b: number): number { while (b !== 0) { [a, b] = [b, a % b]; } return a; } function lcm(a: number, b: number): number { return (a / gcd(a, b)) * b; } ``` ## Python ```python def gcd(a, b): while b != 0: a, b = b, a % b return a def lcm(a, b): return a // gcd(a, b) * b ``` ## PHP ```php function gcdEuclid(int $a, int $b): int { while ($b !== 0) { [$a, $b] = [$b, $a % $b]; } return $a; } function lcm(int $a, int $b): int { return intdiv($a, gcdEuclid($a, $b)) * $b; } ``` ## Java ```java public static int gcd(int a, int b) { while (b != 0) { int rest = a % b; a = b; b = rest; } return a; } public static long lcm(int a, int b) { return (long) (a / gcd(a, b)) * b; // divide first: the product can overflow } ``` ## C# ```csharp public static int Gcd(int a, int b) { while (b != 0) { int rest = a % b; a = b; b = rest; } return a; } public static long Lcm(int a, int b) { return (long) (a / Gcd(a, b)) * b; // divide first: the product can overflow } ``` --- title: "Matrix traversal (spiral / diagonal)" url: https://algopath.pro/patterns/matrix-traversal language: en summary: "Walk a grid in an order that the plain nested loops do not give you. Bounds or a direction rule decide the cell after this one." updated: 2026-10-15 --- # Matrix traversal (spiral / diagonal) Walk a grid in an order that the plain nested loops do not give you. Bounds or a direction rule decide the cell after this one. ## How does Matrix traversal (spiral / diagonal) work? A spiral keeps four bounds: top, bottom, left and right. They close inward as each ring finishes. Walk the top row from left to right, then move the top bound down. That row is now used up. Walk the right column, then the bottom row, then the left column. Each walk shrinks its own bound. Check the bounds before the last two walks. A single leftover row would otherwise be walked twice. A diagonal walk groups cells by the sum of their two indices. Every cell on one diagonal shares that sum. The direction flips on each successive diagonal. That is what produces the zigzag. - `top row: 1, 2, 3` A three by three grid, walked as a spiral. - `right column: 6, 9` Top to bottom, then the right bound moves left. - `bottom row: 8, 7` Right to left, then the bottom bound moves up. - `left column: 4` Bottom to top, then the left bound moves right. - `centre: 5` One cell is left. The four bounds have met. ## When should you use Matrix traversal (spiral / diagonal)? Use Matrix traversal (spiral / diagonal) when the problem statement says “read or fill a grid in spiral order”, “walk a matrix diagonal by diagonal” or “rotate a matrix 90 degrees”. A solution built on it usually runs in O(mn). - read or fill a grid in spiral order - walk a matrix diagonal by diagonal - rotate a matrix 90 degrees - visit every cell in a fixed geometric order ## What is Matrix traversal (spiral / diagonal) confused with? Matrix traversal (spiral / diagonal) is most often confused with Matrix search (sorted grid), Matrix word search (grid DFS/backtracking), Graph BFS / DFS and In-place array transform. - **Matrix search (sorted grid)** - That uses the sorted order to skip most of the grid. This one visits every single cell. - **Matrix word search (grid DFS/backtracking)** - That searches with backtracking and can turn around. Here the path is fixed in advance. - **Graph BFS / DFS** - A traversal follows edges out from a start node. Here the order comes from geometry. - **In-place array transform** - Rotating a matrix in place is that idea in two dimensions. The buffer rule is the same. ## What is the time and space complexity of Matrix traversal (spiral / diagonal)? The target complexity of Matrix traversal (spiral / diagonal) is O(mn). An r by c grid gives O(rc), because each cell is visited once. Space is O(1) beyond the output. ## A worked example of Matrix traversal (spiral / diagonal) ### Rotate a square matrix in place Rotate an n by n matrix ninety degrees clockwise, in place. Allocating a second matrix is not allowed. Transpose it first, swapping each cell with its mirror across the diagonal. Then reverse every row. Those two moves together make a rotation. ```javascript function rotate(matrix) { const n = matrix.length; // transpose: j starts at i + 1, so no swap is undone for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]]; } } for (const row of matrix) row.reverse(); return matrix; } ``` ## Common mistakes with Matrix traversal (spiral / diagonal) - **Transposing over the whole grid** Every swap is then undone by its own mirror. Start the inner loop at i plus one. - **Walking a leftover row twice** A single middle row gets read forwards and then backwards. Check the bounds before the last two walks. - **Assuming the grid is square** A spiral over a wide grid runs out of columns first. Bounds handle that, a fixed loop count does not. - **Swapping row and column indices** The usual order is matrix row then column. Swapping them silently transposes everything. ## Which interview problems use Matrix traversal (spiral / diagonal)? Interview problems solved with Matrix traversal (spiral / diagonal) include Spiral matrix, Spiral matrix II, Rotate image, Diagonal traverse, Set matrix zeroes, Transpose matrix and Zigzag conversion. - **Spiral matrix** Four bounds that close inward ring by ring. - **Spiral matrix II** Fill a grid in that order instead of reading it. - **Rotate image** Transpose, then reverse each row. - **Diagonal traverse** Group the cells by the sum of their indices. - **Set matrix zeroes** Use the first row and column as the marks. - **Transpose matrix** The building block that a rotation is made of. - **Zigzag conversion** The same alternating direction, over a string. ## JavaScript ```javascript function spiralOrder(matrix) { const result = []; let top = 0, bottom = matrix.length - 1; let left = 0, right = matrix[0].length - 1; while (top <= bottom && left <= right) { for (let c = left; c <= right; c++) result.push(matrix[top][c]); for (let r = top + 1; r <= bottom; r++) result.push(matrix[r][right]); if (top < bottom) for (let c = right - 1; c >= left; c--) result.push(matrix[bottom][c]); if (left < right) for (let r = bottom - 1; r > top; r--) result.push(matrix[r][left]); top++; bottom--; left++; right--; } return result; } ``` ## TypeScript ```typescript function spiralOrder(matrix: number[][]): number[] { const result: number[] = []; let top = 0, bottom = matrix.length - 1; let left = 0, right = matrix[0].length - 1; while (top <= bottom && left <= right) { for (let c = left; c <= right; c++) result.push(matrix[top][c]); for (let r = top + 1; r <= bottom; r++) result.push(matrix[r][right]); if (top < bottom) for (let c = right - 1; c >= left; c--) result.push(matrix[bottom][c]); if (left < right) for (let r = bottom - 1; r > top; r--) result.push(matrix[r][left]); top++; bottom--; left++; right--; } return result; } ``` ## Python ```python def spiral_order(matrix): result = [] top, bottom = 0, len(matrix) - 1 left, right = 0, len(matrix[0]) - 1 while top <= bottom and left <= right: result += [matrix[top][c] for c in range(left, right + 1)] result += [matrix[r][right] for r in range(top + 1, bottom + 1)] if top < bottom: result += [matrix[bottom][c] for c in range(right - 1, left - 1, -1)] if left < right: result += [matrix[r][left] for r in range(bottom - 1, top, -1)] top += 1; bottom -= 1; left += 1; right -= 1 return result ``` ## PHP ```php function spiralOrder(array $matrix): array { $result = []; $top = 0; $bottom = count($matrix) - 1; $left = 0; $right = count($matrix[0]) - 1; while ($top <= $bottom && $left <= $right) { for ($c = $left; $c <= $right; $c++) $result[] = $matrix[$top][$c]; for ($r = $top + 1; $r <= $bottom; $r++) $result[] = $matrix[$r][$right]; if ($top < $bottom) for ($c = $right - 1; $c >= $left; $c--) $result[] = $matrix[$bottom][$c]; if ($left < $right) for ($r = $bottom - 1; $r > $top; $r--) $result[] = $matrix[$r][$left]; $top++; $bottom--; $left++; $right--; } return $result; } ``` ## Java ```java public static List spiralOrder(int[][] matrix) { List result = new ArrayList<>(); int top = 0, bottom = matrix.length - 1; int left = 0, right = matrix[0].length - 1; while (top <= bottom && left <= right) { for (int c = left; c <= right; c++) result.add(matrix[top][c]); for (int r = top + 1; r <= bottom; r++) result.add(matrix[r][right]); if (top < bottom) for (int c = right - 1; c >= left; c--) result.add(matrix[bottom][c]); if (left < right) for (int r = bottom - 1; r > top; r--) result.add(matrix[r][left]); top++; bottom--; left++; right--; } return result; } ``` ## C# ```csharp public static List SpiralOrder(int[][] matrix) { var result = new List(); int top = 0, bottom = matrix.Length - 1; int left = 0, right = matrix[0].Length - 1; while (top <= bottom && left <= right) { for (int c = left; c <= right; c++) result.Add(matrix[top][c]); for (int r = top + 1; r <= bottom; r++) result.Add(matrix[r][right]); if (top < bottom) for (int c = right - 1; c >= left; c--) result.Add(matrix[bottom][c]); if (left < right) for (int r = bottom - 1; r > top; r--) result.Add(matrix[r][left]); top++; bottom--; left++; right--; } return result; } ``` --- title: "Matrix search (sorted grid)" url: https://algopath.pro/patterns/matrix-search language: en summary: "In a grid sorted both ways, one corner can only move in a single direction. Each comparison drops a whole row or column." updated: 2026-10-17 --- # Matrix search (sorted grid) In a grid sorted both ways, one corner can only move in a single direction. Each comparison drops a whole row or column. ## How does Matrix search (sorted grid) work? There are two kinds of sorted grid. In one each row continues the previous, in the other rows and columns merely rise. The first kind is a sorted list folded into rows. Binary search over r times c positions. For the second kind, start at the top-right corner. Everything to its left is smaller and everything below is larger. If the value there is too large, move left. That drops the whole column at once. If it is too small, move down. That drops the whole row. Every step removes one row or one column. So the walk ends within r plus c steps. Sources: [Wikipedia: Binary search](https://en.wikipedia.org/wiki/Binary_search) - `start at 7, top right` Looking for 5 in a grid that rises both ways. - `7 is too large` Everything below 7 in that column is larger. Move left. - `4 is too small` Everything left of 4 in that row is smaller. Move down. - `now at 5` The value under the walk matches the target. - `three steps` Nine cells, three comparisons. Two lines were dropped whole. ## When should you use Matrix search (sorted grid)? Use Matrix search (sorted grid) when the problem statement says “matrix sorted along both rows and columns”, “search for a value in a 2D sorted grid” or “start from a corner and eliminate a row or column”. A solution built on it usually runs in O(m+n). - matrix sorted along both rows and columns - search for a value in a 2D sorted grid - start from a corner and eliminate a row or column - row and column both monotonic ## What is Matrix search (sorted grid) confused with? Matrix search (sorted grid) is most often confused with Binary search (array), Two pointers (opposite ends), Matrix traversal (spiral / diagonal) and Linear search. - **Binary search (array)** - When each row continues the last, the grid is one sorted list. A plain binary search then works. - **Two pointers (opposite ends)** - The staircase walk is that idea in two dimensions. Exactly one index moves per step. - **Matrix traversal (spiral / diagonal)** - That visits every cell in a chosen order. Here most cells are never read at all. - **Linear search** - A scan works and costs O(rc). The sorted structure is what makes it wasteful. ## What is the time and space complexity of Matrix search (sorted grid)? The target complexity of Matrix search (sorted grid) is O(m+n). An r by c grid gives O(r + c) for the staircase walk. A fully sorted grid allows O(log rc). ## A worked example of Matrix search (sorted grid) ### The kth smallest value in a sorted grid A grid has rows and columns that both rise. Return its kth smallest value. Flattening and sorting works, but it reads every cell. Binary search over the range of values, not over the positions. For a candidate value, count how many cells are not larger. That count says which half to keep. ```javascript function kthSmallest(matrix, k) { const n = matrix.length; let lo = matrix[0][0]; let hi = matrix[n - 1][n - 1]; const countNotAbove = (value) => { let count = 0; let row = n - 1; // one staircase walk, starting from the bottom-left corner for (let col = 0; col < n; col++) { while (row >= 0 && matrix[row][col] > value) row--; count += row + 1; } return count; }; while (lo < hi) { const mid = lo + Math.floor((hi - lo) / 2); if (countNotAbove(mid) >= k) hi = mid; else lo = mid + 1; } return lo; } ``` ## Common mistakes with Matrix search (sorted grid) - **Starting from the wrong corner** The top-left cell is smaller than both neighbours, so it gives no direction. Start top-right or bottom-left. - **Treating a row-wise grid as fully sorted** Searching by position needs each row to continue the last. Check that before flattening. - **Sorting the whole grid** That throws away the structure you were handed. It also costs rc log rc. - **Returning the middle candidate** In a value search the answer must be a value that exists. Squeeze until the bounds meet. ## Which interview problems use Matrix search (sorted grid)? Interview problems solved with Matrix search (sorted grid) include Search a 2D matrix, Search a 2D matrix II, Kth smallest in a sorted matrix, Find a peak element II, Count negative numbers in a sorted matrix, Median of a row-wise sorted matrix and Smallest common element in all rows. - **Search a 2D matrix** One sorted list folded into rows. - **Search a 2D matrix II** The staircase walk from a corner. - **Kth smallest in a sorted matrix** Binary search over values, counting per candidate. - **Find a peak element II** Binary search over the columns. - **Count negative numbers in a sorted matrix** The same staircase, counting instead of matching. - **Median of a row-wise sorted matrix** Count how many cells are not above a candidate. - **Smallest common element in all rows** One pointer per row, moved together. ## JavaScript ```javascript function searchMatrix(matrix, target) { let row = 0; let col = matrix[0].length - 1; while (row < matrix.length && col >= 0) { const v = matrix[row][col]; if (v === target) return true; if (v > target) col--; else row++; } return false; } ``` ## TypeScript ```typescript function searchMatrix(matrix: number[][], target: number): boolean { let row = 0; let col = matrix[0].length - 1; while (row < matrix.length && col >= 0) { const v = matrix[row][col]; if (v === target) return true; if (v > target) col--; else row++; } return false; } ``` ## Python ```python def search_matrix(matrix, target): row, col = 0, len(matrix[0]) - 1 while row < len(matrix) and col >= 0: v = matrix[row][col] if v == target: return True if v > target: col -= 1 else: row += 1 return False ``` ## PHP ```php function searchMatrix(array $matrix, int $target): bool { $row = 0; $col = count($matrix[0]) - 1; while ($row < count($matrix) && $col >= 0) { $v = $matrix[$row][$col]; if ($v === $target) return true; if ($v > $target) $col--; else $row++; } return false; } ``` ## Java ```java public static boolean searchMatrix(int[][] matrix, int target) { int row = 0; int col = matrix[0].length - 1; while (row < matrix.length && col >= 0) { int v = matrix[row][col]; if (v == target) return true; if (v > target) col--; else row++; } return false; } ``` ## C# ```csharp public static bool SearchMatrix(int[][] matrix, int target) { int row = 0; int col = matrix[0].Length - 1; while (row < matrix.Length && col >= 0) { int v = matrix[row][col]; if (v == target) return true; if (v > target) col--; else row++; } return false; } ``` --- title: "Matrix word search (grid DFS/backtracking)" url: https://algopath.pro/patterns/matrix-word-search language: en summary: "Search a grid by stepping into a neighbour and then stepping back out again. The cell stays marked while you are inside it." updated: 2026-10-17 --- # Matrix word search (grid DFS/backtracking) Search a grid by stepping into a neighbour and then stepping back out again. The cell stays marked while you are inside it. ## How does Matrix word search (grid DFS/backtracking) work? Any cell can start the word. Try the search from each of them. At a cell, compare it with the current character. A mismatch ends this branch at once. Mark the cell as used before stepping onward. Otherwise the path can walk over itself. Try each of the four neighbours in turn. Any of them might continue the word. Unmark the cell after all four calls return. The next search has to see it free. Reaching the end of the word is success. Nothing below that point matters. Sources: [Wikipedia: Backtracking](https://en.wikipedia.org/wiki/Backtracking) - `start at A, index 0` The grid is A, B over C, D and the word is ABD. - `neighbour B, index 1` B matches the second letter, so it is marked too. - `from B: A is marked` The mark is what stops the path from turning back. - `D, index 2` D matches the last letter. The word is complete. - `answer = true` Two cells were marked, and both clear on the way out. ## When should you use Matrix word search (grid DFS/backtracking)? Use Matrix word search (grid DFS/backtracking) when the problem statement says “does the word exist as a path on the board”, “trace letters through adjacent cells, no reuse” or “DFS with backtracking on a grid”. A solution built on it usually runs in O(mn*4^L). - does the word exist as a path on the board - trace letters through adjacent cells, no reuse - DFS with backtracking on a grid - mark a cell as visited, then undo it ## What is Matrix word search (grid DFS/backtracking) confused with? Matrix word search (grid DFS/backtracking) is most often confused with Backtracking, Graph BFS / DFS, Connected components and Trie (prefix tree). - **Backtracking** - This is backtracking with the grid as the tree. The choices are the four neighbours. - **Graph BFS / DFS** - A traversal marks a cell once and forever. Here the mark comes off on the way out. - **Connected components** - Flood fill also walks neighbours, but it never unmarks. It is counting rather than searching. - **Trie (prefix tree)** - With many words to find, a trie prunes the walk. Without one each word costs its own search. ## What is the time and space complexity of Matrix word search (grid DFS/backtracking)? The target complexity of Matrix word search (grid DFS/backtracking) is O(mn*4^L). An r by c grid with a word of length L costs O(rc times 4^L). Pruning is what makes it run. ## A worked example of Matrix word search (grid DFS/backtracking) ### Find every word hidden in a grid You get a grid of letters and a list of words. Return every word the grid contains. Searching each word separately repeats an enormous amount of work. Put all the words into a trie first. Then walk the grid once, carrying a trie node instead of a word index. A missing child prunes the branch. ```javascript function findWords(board, words) { const root = { children: {} }; for (const word of words) { let node = root; for (const c of word) { node.children[c] = node.children[c] ?? { children: {} }; node = node.children[c]; } node.word = word; } const found = []; function walk(r, c, node) { const letter = board[r]?.[c]; const next = letter && node.children[letter]; if (!next) return; // no word continues this way if (next.word) { found.push(next.word); next.word = null; // never report the same word twice } board[r][c] = "#"; // marked only while we are inside this cell walk(r + 1, c, next); walk(r - 1, c, next); walk(r, c + 1, next); walk(r, c - 1, next); board[r][c] = letter; } for (let r = 0; r < board.length; r++) { for (let c = 0; c < board[0].length; c++) walk(r, c, root); } return found; } ``` ## Common mistakes with Matrix word search (grid DFS/backtracking) - **Forgetting to unmark the cell** A marked cell then stays blocked for every later search. Restore it after the four calls return. - **Marking with a real letter** A marker that can appear in a word lets the path reuse a cell. Pick a character the alphabet never holds. - **Checking the bounds after reading** Reading outside the grid throws or gives undefined. Test the coordinates before touching them. - **Searching each word on its own** A hundred words means a hundred full walks. One trie collapses them into a single walk. ## Which interview problems use Matrix word search (grid DFS/backtracking)? Interview problems solved with Matrix word search (grid DFS/backtracking) include Word search, Word search II, Number of islands, Path with maximum gold, Unique paths III, Rat in a maze and Sudoku solver. - **Word search** One word, four neighbours at every step. - **Word search II** A trie prunes the walk when there are many words. - **Number of islands** The same walk, except it never unmarks. - **Path with maximum gold** Collect the value, then restore it on the way out. - **Unique paths III** Every free cell has to be visited exactly once. - **Rat in a maze** The same shape, but the path itself is recorded. - **Sudoku solver** A grid where the choices are digits, not directions. ## JavaScript ```javascript function exist(board, word) { const rows = board.length, cols = board[0].length; function dfs(r, c, i) { if (i === word.length) return true; if (r < 0 || r >= rows || c < 0 || c >= cols || board[r][c] !== word[i]) return false; const tmp = board[r][c]; board[r][c] = "#"; const found = dfs(r + 1, c, i + 1) || dfs(r - 1, c, i + 1) || dfs(r, c + 1, i + 1) || dfs(r, c - 1, i + 1); board[r][c] = tmp; return found; } for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (dfs(r, c, 0)) return true; } } return false; } ``` ## TypeScript ```typescript function exist(board: string[][], word: string): boolean { const rows = board.length, cols = board[0].length; function dfs(r: number, c: number, i: number): boolean { if (i === word.length) return true; if (r < 0 || r >= rows || c < 0 || c >= cols || board[r][c] !== word[i]) return false; const tmp = board[r][c]; board[r][c] = "#"; const found = dfs(r + 1, c, i + 1) || dfs(r - 1, c, i + 1) || dfs(r, c + 1, i + 1) || dfs(r, c - 1, i + 1); board[r][c] = tmp; return found; } for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (dfs(r, c, 0)) return true; } } return false; } ``` ## Python ```python def exist(board, word): rows, cols = len(board), len(board[0]) def dfs(r, c, i): if i == len(word): return True if r < 0 or r >= rows or c < 0 or c >= cols or board[r][c] != word[i]: return False tmp, board[r][c] = board[r][c], "#" found = (dfs(r + 1, c, i + 1) or dfs(r - 1, c, i + 1) or dfs(r, c + 1, i + 1) or dfs(r, c - 1, i + 1)) board[r][c] = tmp return found return any(dfs(r, c, 0) for r in range(rows) for c in range(cols)) ``` ## PHP ```php function exist(array &$board, string $word): bool { $rows = count($board); $cols = count($board[0]); $dfs = function (int $r, int $c, int $i) use (&$board, &$dfs, $word, $rows, $cols): bool { if ($i === strlen($word)) return true; if ($r < 0 || $r >= $rows || $c < 0 || $c >= $cols || $board[$r][$c] !== $word[$i]) return false; $tmp = $board[$r][$c]; $board[$r][$c] = "#"; $found = $dfs($r + 1, $c, $i + 1) || $dfs($r - 1, $c, $i + 1) || $dfs($r, $c + 1, $i + 1) || $dfs($r, $c - 1, $i + 1); $board[$r][$c] = $tmp; return $found; }; for ($r = 0; $r < $rows; $r++) { for ($c = 0; $c < $cols; $c++) { if ($dfs($r, $c, 0)) return true; } } return false; } ``` ## Java ```java public static boolean exist(char[][] board, String word) { for (int r = 0; r < board.length; r++) { for (int c = 0; c < board[0].length; c++) { if (dfs(board, word, r, c, 0)) return true; } } return false; } private static boolean dfs(char[][] board, String word, int r, int c, int i) { if (i == word.length()) return true; if (r < 0 || r >= board.length || c < 0 || c >= board[0].length || board[r][c] != word.charAt(i)) return false; char tmp = board[r][c]; board[r][c] = '#'; boolean found = dfs(board, word, r + 1, c, i + 1) || dfs(board, word, r - 1, c, i + 1) || dfs(board, word, r, c + 1, i + 1) || dfs(board, word, r, c - 1, i + 1); board[r][c] = tmp; return found; } ``` ## C# ```csharp public static bool Exist(char[][] board, string word) { for (int r = 0; r < board.Length; r++) { for (int c = 0; c < board[0].Length; c++) { if (Dfs(board, word, r, c, 0)) return true; } } return false; } private static bool Dfs(char[][] board, string word, int r, int c, int i) { if (i == word.Length) return true; if (r < 0 || r >= board.Length || c < 0 || c >= board[0].Length || board[r][c] != word[i]) return false; char tmp = board[r][c]; board[r][c] = '#'; bool found = Dfs(board, word, r + 1, c, i + 1) || Dfs(board, word, r - 1, c, i + 1) || Dfs(board, word, r, c + 1, i + 1) || Dfs(board, word, r, c - 1, i + 1); board[r][c] = tmp; return found; } ``` --- title: "Fenwick tree / segment tree" url: https://algopath.pro/patterns/fenwick-segment-tree language: en summary: "A tree built over the ranges answers a query and accepts an update, both in log n. Prefix sums cannot do the second one." updated: 2026-10-17 --- # Fenwick tree / segment tree A tree built over the ranges answers a query and accepts an update, both in log n. Prefix sums cannot do the second one. ## How does Fenwick tree / segment tree work? A Fenwick tree stores partial sums in an array indexed from 1. tree[i] covers a block that ends at i. The block length is the lowest set bit of i, written i & -i. So tree[4] covers 1 to 4, and tree[6] covers 5 and 6. query(i) adds tree[i], then removes the lowest bit from i. It repeats until i reaches zero and returns the prefix sum. update(i) adds the change to tree[i], then adds the lowest bit to i. Every block that contains position i gets the change. Both loops touch about log n cells. A range sum is query(r) minus query(l - 1). A segment tree keeps one node per range, and its sum comes from two children. A query stops at every node that lies fully inside the range. A segment tree takes more code than a Fenwick tree. In return it also handles min, max and lazy range updates. Sources: [Wikipedia: Fenwick tree](https://en.wikipedia.org/wiki/Fenwick_tree), [Wikipedia: Segment tree](https://en.wikipedia.org/wiki/Segment_tree), [Fenwick (1994), A new data structure for cumulative frequency tables](https://doi.org/10.1002/spe.4380240306) - `tree = [3, 4, 4, 9]` The array is [3, 1, 4, 1], indexed from 1. tree[4] covers all four. - `query(3): tree[3] + tree[2]` 3 loses its lowest bit and becomes 2. So far 4 plus 4. - `query(3) = 8` 2 loses its lowest bit and becomes 0. The sum 8 is 3 + 1 + 4. - `update(2, +5): tree[2] and tree[4]` 2 plus its lowest bit is 4, and 8 is past the end. Two cells change. - `sum of 2 to 3 = 13 - 3 = 10` query(3) is now 13 and query(1) is 3. Subtracting gives the range. ## When should you use Fenwick tree / segment tree? Use Fenwick tree / segment tree when the problem statement says “range sum or query with point updates interleaved”, “live leaderboard or running rank while scores change” or “count inversions or how many earlier values are larger”. A solution built on it usually runs in O(log n) per op. - range sum or query with point updates interleaved - live leaderboard or running rank while scores change - count inversions or how many earlier values are larger - range queries while the data keeps changing ## What is Fenwick tree / segment tree confused with? Fenwick tree / segment tree is most often confused with Prefix sums, Difference array, Sweep line (event counting) and Binary search (array). - **Prefix sums** - Prefix sums read faster but break on any change. This pays log n to allow updates. - **Difference array** - That takes many range updates and one read at the end. Here reads and writes are mixed. - **Sweep line (event counting)** - A sweep processes events in sorted order and never looks back. Here queries arrive in any order. - **Binary search (array)** - Both halve a range each step. One hunts a value, this one aggregates over a range. ## What is the time and space complexity of Fenwick tree / segment tree? The target complexity of Fenwick tree / segment tree is O(log n) per op. n up to 1e6 with q mixed operations gives O((n + q) log n). Memory is O(n) for a Fenwick tree. ## A worked example of Fenwick tree / segment tree ### Count the smaller values to the right For every element, count how many later elements are smaller than it. A nested loop is O(n squared) and dies at 1e5 elements. Walk the array from the right, keeping a Fenwick tree over the values. For each element, ask how many smaller values are recorded already. Then record this one. ```javascript function countSmaller(nums) { const sorted = [...new Set(nums)].sort((a, b) => a - b); const rank = new Map(sorted.map((v, i) => [v, i + 1])); // ranks start at 1 const tree = new Array(sorted.length + 1).fill(0); const add = (i) => { for (; i < tree.length; i += i & -i) tree[i]++; }; const countBelow = (i) => { let total = 0; for (; i > 0; i -= i & -i) total += tree[i]; return total; }; const answer = new Array(nums.length); for (let i = nums.length - 1; i >= 0; i--) { const r = rank.get(nums[i]); answer[i] = countBelow(r - 1); // strictly smaller values already seen add(r); } return answer; } ``` ## Common mistakes with Fenwick tree / segment tree - **Indexing a Fenwick tree from zero** The low-bit step needs indices to start at one. A zero index loops forever. - **Using it where nothing ever changes** A prefix array answers in O(1) and costs nothing to read. Pay for a tree only when updates arrive. - **Forgetting to compress the values** A tree sized by the value range dies on values up to 1e9. Map them to ranks first. - **Combining ranges with a rule that does not fit** Sum and min work because order does not matter to them. An order-dependent rule needs more per node. ## Which interview problems use Fenwick tree / segment tree? Interview problems solved with Fenwick tree / segment tree include Range sum query mutable, Count of smaller numbers after self, Count of range sum, Reverse pairs, Range minimum query, Range sum query 2D mutable and My calendar II. - **Range sum query mutable** Reads and writes mixed together in any order. - **Count of smaller numbers after self** A Fenwick tree over compressed ranks. - **Count of range sum** Prefix sums fed into the tree. - **Reverse pairs** Merge sort or a Fenwick tree, either works. - **Range minimum query** A segment tree, since min has no inverse. - **Range sum query 2D mutable** A tree whose nodes are themselves trees. - **My calendar II** A segment tree with lazy range updates. ## JavaScript ```javascript class Fenwick { constructor(n) { this.tree = new Array(n + 1).fill(0); } update(i, delta) { for (; i < this.tree.length; i += i & -i) this.tree[i] += delta; } query(i) { let sum = 0; for (; i > 0; i -= i & -i) sum += this.tree[i]; return sum; } } ``` ## TypeScript ```typescript class Fenwick { private tree: number[]; constructor(n: number) { this.tree = new Array(n + 1).fill(0); } update(i: number, delta: number): void { for (; i < this.tree.length; i += i & -i) this.tree[i] += delta; } query(i: number): number { let sum = 0; for (; i > 0; i -= i & -i) sum += this.tree[i]; return sum; } } ``` ## Python ```python class Fenwick: def __init__(self, n): self.tree = [0] * (n + 1) def update(self, i, delta): while i < len(self.tree): self.tree[i] += delta i += i & -i def query(self, i): total = 0 while i > 0: total += self.tree[i] i -= i & -i return total ``` ## PHP ```php class Fenwick { private array $tree; public function __construct(int $n) { $this->tree = array_fill(0, $n + 1, 0); } public function update(int $i, int $delta): void { for (; $i < count($this->tree); $i += $i & -$i) $this->tree[$i] += $delta; } public function query(int $i): int { $sum = 0; for (; $i > 0; $i -= $i & -$i) $sum += $this->tree[$i]; return $sum; } } ``` ## Java ```java public static class Fenwick { private final int[] tree; public Fenwick(int n) { tree = new int[n + 1]; } public void update(int i, int delta) { for (; i < tree.length; i += i & -i) tree[i] += delta; } public int query(int i) { int sum = 0; for (; i > 0; i -= i & -i) sum += tree[i]; return sum; } } ``` ## C# ```csharp public class Fenwick { private readonly int[] tree; public Fenwick(int n) { tree = new int[n + 1]; } public void Update(int i, int delta) { for (; i < tree.Length; i += i & -i) tree[i] += delta; } public int Query(int i) { int sum = 0; for (; i > 0; i -= i & -i) sum += tree[i]; return sum; } } ``` ## Free lessons - [What is an algorithm?](https://algopath.pro/steps/what-is-an-algorithm) - [Instructions a computer follows exactly](https://algopath.pro/steps/instructions-a-computer-follows) - [Data and variables: a labeled box](https://algopath.pro/steps/data-and-variables) - [A list: a row of numbered boxes](https://algopath.pro/steps/a-list-of-boxes) - [Doing it for everyone: loops](https://algopath.pro/steps/loops-do-it-for-each) - [Making choices: if / else](https://algopath.pro/steps/making-choices-if-else) - [Your first program: add two numbers](https://algopath.pro/steps/first-program-add-two-numbers) - [Find the biggest number](https://algopath.pro/steps/find-the-biggest-number) - [Why some ways are faster](https://algopath.pro/steps/why-some-ways-are-faster) - [Big-O: a name for how fast the work grows](https://algopath.pro/steps/big-o-and-constraints) - [Rearranging an array without a second one](https://algopath.pro/steps/arrays-strings-in-place) - [Sets: a bag that refuses duplicates](https://algopath.pro/steps/collections-sets) - [Map: a labeled store of key → value](https://algopath.pro/steps/collections-maps) - [WeakMap and WeakSet: attach data to objects](https://algopath.pro/steps/collections-weak) - [How a hash table finds a slot](https://algopath.pro/steps/hashing-buckets) - [Collisions, handled by chaining](https://algopath.pro/steps/hashing-chaining) - [Collisions, handled by probing (open addressing)](https://algopath.pro/steps/hashing-open-addressing) - [Load factor and resizing](https://algopath.pro/steps/hashing-resize) - ["Have I seen this?" in one step](https://algopath.pro/steps/hashing-map-set-frequency) - [Closing in from both ends](https://algopath.pro/steps/two-pointers-opposite-ends) - [Squeezing out duplicates in place](https://algopath.pro/steps/two-pointers-same-direction) - [Reusing the last window's work](https://algopath.pro/steps/sliding-window-fixed) - [A window that changes size to fit a rule](https://algopath.pro/steps/sliding-window-variable) - [A window that remembers what's inside it](https://algopath.pro/steps/sliding-window-hash-counter) - [Adding up any stretch of a list in one step](https://algopath.pro/steps/prefix-sums) - [Counting the stretches that add up to k](https://algopath.pro/steps/subarray-sum-k) - [Summing any rectangle in one step](https://algopath.pro/steps/prefix-2d) - [Bumping a whole range without touching every slot](https://algopath.pro/steps/difference-arrays) - [Finding an item by scanning a list](https://algopath.pro/steps/linear-search) - [Finding one item in a sorted list by halving](https://algopath.pro/steps/binary-search-array) - [Finding the exact spot where "no" turns into "yes"](https://algopath.pro/steps/binary-search-boundary) - [Binary search in a rotated sorted array](https://algopath.pro/steps/search-rotated) - [Guessing the answer, then binary-searching the guess](https://algopath.pro/steps/binary-search-on-answer) ## Articles - [seo.sections.blog.index · How to prepare](https://algopath.pro/blog/category/how-to-prepare) - [seo.sections.blog.index · Algorithms step by step](https://algopath.pro/blog/category/algorithms) - [seo.sections.blog.index · Product updates](https://algopath.pro/blog/category/product-updates) --- title: "How does a hash table work" url: https://algopath.pro/blog/how-does-a-hash-table-work language: en summary: "A hash table turns a key into an array index, and that one step explains every rule you were told to follow. Constant time is an average." updated: 2026-10-27 --- # How does a hash table work **Short answer.** A hash table runs a key through a hash function, takes the result modulo the array size and stores the value in that slot. A lookup repeats the same steps, so it takes constant time on average. Collisions are handled by chaining or open addressing, and the table doubles its array past a load factor of about 0.75. ## The core move Keep an array of some size, say 16 slots. Run the key through a hash function. You get a number. Take it modulo 16. You have a slot. Put the value in it. Looking it up repeats those steps and lands in the same place. No searching happened. That is where constant time comes from. The [hash function](https://en.wikipedia.org/wiki/Hash_table) has one job. Spread keys evenly across slots. The hash function always gives the same answer for the same key. ## Collisions Two keys will land in the same slot. With 16 slots and 20 keys it is guaranteed. Long before that it is likely. Two standard fixes handle key collisions. **Chaining.** Each slot holds a small list. Collide, and you append. Lookup finds the slot, then walks a list. Normally one or two items. **Open addressing.** Slot taken? Try the next one. Keep going until one is free. No lists. Better cache behaviour. More delicate deletion. Both are still fast, as long as the lists stay short. ## Load factor and resizing The load factor keeps the lists short. It is entries divided by slots. Past roughly 0.75, collisions get common and lookups slow down. The table allocates a bigger array, usually double. Then it rehashes every key into it. That resize is O(n). One unlucky insert pays for it. Averaged over all inserts, the cost is still constant. That is the honest meaning of [amortised](https://en.wikipedia.org/wiki/Amortized_analysis) time. What constant time means is in [Big O explained simply](/blog/big-o-notation-explained-simply). Constant time explains a production surprise too. An insert that usually takes a microsecond occasionally takes far longer. That matters if you hold a lock while it happens. ## Why the worst case is O(n) If every key hashes to the same slot, the table becomes one long list. Every lookup scans it. With random data this does not happen. With chosen data it does. Chosen-key collisions have been used as a [collision attack](https://en.wikipedia.org/wiki/Collision_attack). Send a server thousands of keys that collide on purpose, and watch the CPU burn. Modern runtimes now seed the hash function randomly per process. Your interview answer stays "O(1) average, O(n) worst case". They are checking that you know it is an average. ## Two rules that now make sense **Do not mutate a key after inserting it.** The slot came from the old contents. Change it, and the entry sits in the wrong place. It is unreachable rather than lost, which is worse. **Equal keys must hash equally.** In languages where you write both, override equality and leave the hash alone, and the map starts losing entries. The bug appears months later. ## When to use something else A hash table has no order. That is the trade, not an oversight. "All keys between 10 and 50" is a full scan. So is "the smallest key". So is "everything in order". Those want a [balanced tree](https://en.wikipedia.org/wiki/Self-balancing_binary_search_tree). Log n per operation, ordered by construction. Databases keep both. Hash indexes for exact matches, [B-trees](https://en.wikipedia.org/wiki/B-tree) for ranges. ## Where this sits [AlgoPath](/) spends steps 14 to 17 building one. Collisions and resizing become code you wrote, not a paragraph you read. When to reach for one at all is in [set vs map vs array](/blog/set-vs-map-vs-array). The pattern that runs on it is [hash set and map](/patterns/hash-set-map). The first 33 steps are free. --- title: "Search in a rotated sorted array: why one half is always sorted" url: https://algopath.pro/blog/search-in-rotated-sorted-array language: en summary: "How binary search survives a rotation. The one comparison that finds the sorted half, Python code for the target and the minimum, a traced run, duplicates and the mistakes." updated: 2026-10-25 --- # Search in a rotated sorted array: why one half is always sorted **Short answer.** Binary search still works on a rotated sorted array. One half is always sorted. At each step, find the sorted half. Check whether the target lies inside it. Time O(log n). It is the pattern behind two classic LeetCode problems. Below: the idea, Python code, a traced run and the mistakes. ## What is a rotated sorted array? Take a sorted array [1, 2, 3, 4, 5, 6, 7]. Shift it around by three positions. You get [4, 5, 6, 7, 1, 2, 3]. That is a rotated sorted array. It is two sorted pieces. The drop from a large value to a small one is the pivot point. Here it is the step from 7 to 1. Plain binary search breaks on it. The middle value does not tell you which way to go. ## How does binary search pick a half in a rotated sorted array? Take the middle index mid. Compare nums[lo] with nums[mid]. If nums[lo] is not above nums[mid], the left half is sorted. Otherwise the right half is. Now look at the target. If it falls inside the sorted half by value, go there. Otherwise go to the other half. Each step halves the range. Hence O(log n). ## What does one run look like? Array [4, 5, 6, 7, 0, 1, 2], target 0. lo = 0, hi = 6, mid = 3, nums[mid] = 7. nums[lo] = 4 is not above 7. The left half is sorted: 4 to 7. Target 0 is not in it, go right: lo = 4. mid = 5, nums[mid] = 1. nums[lo] = 0 is not above 1, left half [0, 1] is sorted. Target 0 is in it, hi = 4. mid = 4, nums[mid] = 0. Found, answer 4. ## How do you write it in Python? ``` def search(nums, target): lo, hi = 0, len(nums) - 1 while lo <= hi: mid = (lo + hi) // 2 if nums[mid] == target: return mid if nums[lo] <= nums[mid]: # left half sorted if nums[lo] <= target < nums[mid]: hi = mid - 1 else: lo = mid + 1 else: # right half sorted if nums[mid] < target <= nums[hi]: lo = mid + 1 else: hi = mid - 1 return -1 def find_min(nums): lo, hi = 0, len(nums) - 1 while lo < hi: mid = (lo + hi) // 2 if nums[mid] > nums[hi]: # minimum is right of mid lo = mid + 1 else: hi = mid return nums[lo] ``` The second function compares mid with the right end. If nums[mid] is above nums[hi], the pivot is to the right. Otherwise it is to the left or at mid. ## How do the rotated sorted array problems differ? | Problem | Compare mid with | Duplicates | Time | | --- | --- | --- | --- | | Search in Rotated Sorted Array | nums[lo] and the target | No | O(log n) | | Search in Rotated Sorted Array II | Same, plus a shrink on ties | Yes | O(log n), worst O(n) | | Find Minimum in Rotated Sorted Array | nums[hi] | No | O(log n) | | Find the rotation count | Index of the minimum | No | O(log n) | ## What changes when there are duplicates? Suppose nums[lo], nums[mid] and nums[hi] are all equal. Neither half is provably sorted. Example: [1, 1, 1, 0, 1]. The fix: move lo one step right and hi one step left. Then continue. Worst case O(n), but ordinary inputs stay logarithmic. ## When does a problem need this pattern? - "A sorted array rotated at an unknown pivot." - "Find the target in O(log n)" in such an array. - "Find the minimum" or "how many times was it rotated". - "The array rises, then falls": the neighbouring peak problem. ## How do you find the minimum in a rotated sorted array? Use find_min above. Compare the middle with the right end. If the middle is bigger, the minimum is to the right. Drop the left half and the middle. Otherwise drop the right half. Example [4, 5, 6, 7, 0, 1, 2]. mid = 3, value 7, above 2, so lo = 4. mid = 5, value 1, not above 2, so hi = 5. mid = 4, value 0, hi = 4. Answer 0. ## What mistakes break the search? - Comparing nums[lo] and nums[mid] with strict less-than. A one-element half then counts as unsorted. - Non-strict bounds on both sides of the target check. The loop never ends. - Comparing with nums[lo] instead of nums[hi] in find_min. An unrotated array breaks it. - Finding the pivot with a linear scan. That throws away the whole point. ## How do you explain it in an interview? Say: one half is always sorted. Show the single comparison that finds it. Then say how you decide where the target is. Give O(log n): every step discards half. Mention duplicates as a separate case, even unasked. Drill this pattern on the [binary search on a rotated array](/patterns/binary-search-rotated) page. The base skill lives on the [binary search](/patterns/binary-search-array) page. ## FAQ ### Can you find the pivot first and then search normally? Yes. That is two binary searches instead of one, still O(log n). The one-pass version is shorter. The two-step version is easier to explain. ### How do you find how many positions it was rotated by? That is the index of the minimum. Return lo from find_min instead of nums[lo]. In the array above the minimum sits at index 4. The rotation is 4. ### What if the array is not rotated at all? The code handles it. The left half is always sorted. The search reduces to the plain one. ### How do you find a peak in an array that rises then falls? Compare nums[mid] with nums[mid + 1]. If the right one is bigger, the peak is right. Otherwise left or at mid. Same idea, different comparison. --- title: "BFS vs DFS: how to choose, Python code for graphs and grids, Number of Islands" url: https://algopath.pro/blog/bfs-vs-dfs language: en summary: "One question decides it: do you need the shortest path? The choice table, trigger words, Python code for adjacency lists and grids, Number of Islands and the mistakes." updated: 2026-10-23 --- # BFS vs DFS: how to choose, Python code for graphs and grids, Number of Islands **Short answer.** BFS vs DFS comes down to one question. Do you need the shortest path? BFS walks the graph level by level with a queue. It finds the fewest edges. DFS dives deep with a stack or recursion. It finds components, cycles and orderings. Both cost O(V + E). ## What is a graph traversal? A graph is nodes and edges. A traversal starts at one node and visits everything reachable. It visits each node once, so it needs a visited set. BFS and DFS differ in order. BFS takes all neighbours first, then neighbours of neighbours. DFS takes one neighbour as far as it goes, then comes back. The graph may come as an adjacency list, a matrix or a grid. The traversal code is the same. Only the way you list neighbours changes. ## BFS vs DFS: how do you choose? | Trait | BFS | DFS | | --- | --- | --- | | Structure | Queue | Stack or recursion | | Shortest path, no weights | Yes | No | | Components, islands | Yes | Yes, shorter code | | Cycle detection, topological order | Awkward | Yes | | Memory | Width of a level | Depth of a path | | Risk | Huge queue on a wide graph | Stack overflow on a deep one | The rule is short. The words "shortest" or "minimum number of steps" mean BFS. Everything else, take DFS. ## When does a problem need BFS or DFS? - "Number of islands", "connected components", "flood fill". - "Minimum steps to the exit", "shortest path in a maze". - "Can you get from A to B", "is there a path". - "Cells adjacent on four sides." A grid is a graph too. A cell is a node, its four neighbours are edges. Many matrix problems are traversal problems in disguise. ## What do BFS and DFS look like on one example? Graph: A connects to B and C. B and C connect to D. Start at A. BFS: queue [A]. Pop A, push B and C. Pop B, push D. Pop C, D already seen. Pop D. Order A, B, C, D, distances 0, 1, 1, 2. DFS: go A, then B, then D. D has no new neighbours, return. B is done, return to A. Go to C, D already seen. Order A, B, D, C. ## How do you write BFS and DFS in Python? ``` from collections import deque def bfs(graph, start): dist = {start: 0} queue = deque([start]) while queue: v = queue.popleft() for u in graph[v]: if u not in dist: dist[u] = dist[v] + 1 queue.append(u) return dist def dfs(graph, start, visited=None): visited = visited if visited is not None else set() visited.add(start) for u in graph[start]: if u not in visited: dfs(graph, u, visited) return visited def num_islands(grid): rows, cols = len(grid), len(grid[0]) seen = set() def sink(r, c): stack = [(r, c)] while stack: r, c = stack.pop() for nr, nc in ((r+1, c), (r-1, c), (r, c+1), (r, c-1)): if 0 <= nr < rows and 0 <= nc < cols \ and grid[nr][nc] == "1" and (nr, nc) not in seen: seen.add((nr, nc)) stack.append((nr, nc)) count = 0 for r in range(rows): for c in range(cols): if grid[r][c] == "1" and (r, c) not in seen: seen.add((r, c)) sink(r, c) count += 1 return count ``` In num_islands the DFS uses an explicit stack. On a 1000 by 1000 grid, recursion hits Python's depth limit. ## How does DFS solve Number of Islands? A grid of zeros and ones. An island is a group of ones touching on the sides. Count the islands. Scan every cell. An unvisited one means a new island. Traverse from it and mark the whole island. Count the traversals you started. Each cell is visited once. Time O(rows · cols). ## Why does BFS find the shortest path? The queue processes nodes in order of distance. All nodes at distance 1, then all at distance 2. The first time you reach a node is the shortest way there. This only holds without weights. With weights you need Dijkstra. ## What mistakes break BFS and DFS? - Marking a node visited on leaving the queue, not on entering. One node gets queued many times. - Using list.pop(0) instead of deque. Each pop costs O(n). - Recursive DFS on a deep graph. Python allows about a thousand frames. - Reading a grid cell before checking the bounds. ## How do you explain BFS vs DFS in an interview? Say how the graph is given and what counts as a neighbour. Say BFS or DFS, and why. Give O(V + E): each node and each edge is looked at once. For a grid, mention the explicit stack or queue right away. It shows you thought about input size. Drill this pattern on the [graph BFS / DFS](/patterns/graph-bfs-dfs) page. The neighbouring pattern lives on the [connected components](/patterns/graph-components) page. ## FAQ ### How do you recover the path after BFS? Keep a parent dictionary. When you queue a node, record where you came from. From the target, follow parent back to the start. ### What is multi-source BFS? You queue several start nodes at distance 0. That solves "distance to the nearest zero" in one pass. ### When is a plain traversal the wrong tool? When edges have weights and you need the shortest path: use Dijkstra. When the graph changes between queries: use union-find. --- title: "Dynamic programming explained: state, transition, base and three worked problems" url: https://algopath.pro/blog/dynamic-programming-explained language: en summary: "How to spot a DP problem, the three sentences to say before coding, bottom-up vs top-down, Python code for Climbing Stairs, Coin Change and House Robber, and the mistakes." updated: 2026-10-22 --- # Dynamic programming explained: state, transition, base and three worked problems **Short answer.** Dynamic programming solves a problem through the answers to its smaller versions. The answer for n is built from the answers for n - 1 and n - 2. Each small answer is computed once and stored. "How many ways", "minimum cost" and "maximum profit" problems then run in O(n) or O(n²). ## What is dynamic programming? Take a problem whose big answer is built from small ones. Define a state: what "the answer for i" means. Write a transition: how the answer for i follows from earlier answers. The key condition is overlap. The small problems repeat. Plain recursion recomputes them again and again. Dynamic programming computes each one once. The word "dynamic" means nothing here. The name is historical. The substance is a table of answers. ## When does a problem need dynamic programming? - "In how many ways", "number of paths". - "Minimum cost", "maximum sum with no two adjacent". - "Can you make the amount from these coins", "can you split into two equal halves". - "Longest increasing subsequence." The shared signal: the whole answer depends on answers for its parts. And the parts are few. If each choice is independent of the past, greedy is enough. If you need every solution, that is backtracking. ## What are the three steps of a dynamic programming solution? - State. dp[i] is the answer for the first i elements, or for number i. - Transition. A formula that expresses dp[i] through dp at smaller indexes. - Base. The dp values for the smallest i, known up front. Then comes the order. Compute from the base towards the target. Every transition must read cells that are already filled. ## What does one problem look like? Climbing Stairs: n steps, you climb one or two at a time. How many ways to reach the top? State: dp[i] is the number of ways to reach step i. Transition: dp[i] = dp[i - 1] + dp[i - 2]. You arrive from i - 1 or from i - 2. Base: dp[1] = 1, dp[2] = 2. For n = 5: dp = [_, 1, 2, 3, 5, 8]. Answer 8. Those are Fibonacci numbers, and that is no accident. ## How do you write dynamic programming in Python? ``` def climb_stairs(n): if n <= 2: return n a, b = 1, 2 # dp[i-2], dp[i-1] for _ in range(3, n + 1): a, b = b, a + b return b def coin_change(coins, amount): INF = float("inf") dp = [0] + [INF] * amount # dp[x] = fewest coins for amount x for x in range(1, amount + 1): for c in coins: if c <= x and dp[x - c] + 1 < dp[x]: dp[x] = dp[x - c] + 1 return -1 if dp[amount] == INF else dp[amount] def rob(houses): take = skip = 0 # best with last house taken / skipped for h in houses: take, skip = skip + h, max(take, skip) return max(take, skip) ``` The first function keeps two variables instead of an array. When the transition looks back two steps, the array is not needed. ## Bottom-up or top-down: which one to write? | Trait | Bottom-up (table) | Top-down (memoisation) | | --- | --- | --- | | Code shape | A loop | Recursion with a cache | | Order | You choose it | Recursion chooses it | | Memory | Often shrinks to O(1) | Cache plus call stack | | Wasted work | Sometimes every state | Only states you need | | Risk | Wrong order | Stack overflow | In an interview, start with recursion and a cache. The transition is easier to find that way. Rewrite as a loop if asked. ## How does dynamic programming solve Coin Change? You get coin values and an amount. Return the fewest coins. State: dp[x] is the fewest coins for amount x. Transition: for each coin c, try dp[x - c] + 1. Base: dp[0] = 0. Greedy fails here. Coins [1, 3, 4], amount 6. Greedy picks 4 + 1 + 1. The answer is 3 + 3. That failure is why the table is needed. ## How does it solve House Robber? A row of houses with money. You cannot rob two neighbours. Maximise the total. State: the best result for the first i houses. Transition: rob house i and add the best for i - 2. Or skip it and keep the best for i - 1. The code above holds that in take and skip. ## What mistakes break a dynamic programming solution? - A vague state. The transition then cannot be written in terms of it. - A missing or wrong base. The error spreads through the whole table. - An order where a transition reads an empty cell. - Going greedy where the states have to be searched. ## How do you explain dynamic programming in an interview? Say the state out loud in one sentence. Then the transition. Then the base. Only then write code. The interviewer is listening for those three sentences. Give the complexity as states times transition cost. For Coin Change that is O(amount · coins). Drill this pattern on the [dynamic programming](/patterns/dynamic-programming) page. The top-down form lives on the [recursion with memoization](/patterns/memoization) page. ## FAQ ### How do you know a problem is not greedy? Find an input where the best local choice hurts the total. Coins [1, 3, 4] with amount 6 is one. Found one: you need the table. ### How many states are acceptable? Up to a few million, usually. More than that: look for a different parameter or a different pattern. ### What if you need the actual answer, not just a number? Store the choice that produced dp[i] next to it. At the end, walk the choices backwards and rebuild the answer. ### Where does dynamic programming show up in real systems? DNA alignment and spell checking through edit distance. Data compression and line breaking in text layout. Routing that finds the cheapest path in stages. --- title: "Set vs map vs array: which one to use" url: https://algopath.pro/blog/set-vs-map-vs-array language: en summary: "Pick the container from the question you ask most often. Four questions, the cost table, and the cases where an array still wins." updated: 2026-10-20 --- # Set vs map vs array: which one to use **Short answer.** Pick the container by the question you ask most often. Have I seen this before? A set. What value belongs to this key? A map. What is the smallest so far? A heap. What sits at position i, and does the order matter? An array. A set or a map answers in one step on average. ## Four questions **"Have I seen this before?"** A [set](https://en.wikipedia.org/wiki/Set_(abstract_data_type)). Membership in one step, no matter the size. **"What value belongs to this key?"** An [associative array](https://en.wikipedia.org/wiki/Associative_array). Same speed, and it carries something along. **"What is the smallest so far?"** A [heap](https://en.wikipedia.org/wiki/Heap_(data_structure)). Insert and take the minimum in log n. **"What is at position i, and does the order matter?"** An array. Most problems in the first half of a curriculum are one of these four in a costume. The first two live in [hash set and map](/patterns/hash-set-map). ## The cost of the same operation Finding one element: - Unsorted array: a step per element. - Sorted array: log n with binary search. - Set or map: one step, on average. - Heap: one step for the minimum. A full scan for anything else. That first line quietly turns solutions quadratic. A membership check inside a loop is two nested passes. On screen it looks like one loop. ## When an array still wins A set is not free. It hashes the key, follows a pointer, jumps elsewhere in memory. Under about 100 elements, scanning an array is often faster in real time. Modern [CPU caches](https://en.wikipedia.org/wiki/CPU_cache) are built for contiguous memory. Two more cases. Small integer keys beat a hash map with an indexed array. Which version is allowed is decided by the [constraints](/blog/time-complexity-from-constraints). Order, sorting, and neighbor lookups need an array or a tree. Hashing destroys order by design. ## Three mistakes worth naming **Reaching for a map when a set would do.** Every value is true? You wanted a set. A set is smaller. It also says what you meant. **Object keys in JavaScript.** A plain object turns every key into a string. Object key coercion puts `1` and `"1"` in the same slot. An object as a key becomes "[object Object]". A [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) handles keys that are not strings. **Deleting the wrong thing.** A count of zero still leaves the key in the map. A stale zero breaks a presence check silently. Delete the key when the count reaches zero. ## How to decide in ten seconds Before writing anything, finish this sentence: "the operation I will do thousands of times is _____." Pick the container that does that operation in one step. Say it out loud in an interview. Choosing a structure for a stated reason is what the question tests. ## Where this sits [AlgoPath](/) spends steps 11 to 13 on this one choice. With code you write, before any named pattern. The cost side of the decision is in [Big O explained simply](/blog/big-o-notation-explained-simply). The first 33 steps are free. --- title: "Backtracking algorithm: the three-line template, Python code and N Queens" url: https://algopath.pro/blog/backtracking-algorithm language: en summary: "Search with undo for all permutations, all subsets and N queens. The template, Python code, a traced run, how it differs from DP and the mistakes that break it." updated: 2026-10-18 --- # Backtracking algorithm: the three-line template, Python code and N Queens **Short answer.** A backtracking algorithm builds a solution one choice at a time. A choice leads to a dead end: undo it, try the next one. It solves "all permutations", "all subsets", "N queens" and sudoku. Below: the template, Python code, a traced run and the usual mistakes. ## What is a backtracking algorithm? You hold a partial solution. At each step you pick the next element from the valid options. If the pick leads to a full solution, record it. If it leads nowhere, undo it and pick the next. It is depth-first search over a tree of choices. A node is a partial solution. An edge is one choice. Leaves are full solutions or dead ends. The word backtrack names the main move. Take a step, come back, take a different one. After the return, the state is exactly what it was before the step. The difference from brute force is early pruning. Two queens already attack each other: the other six are never placed. ## When does a problem need a backtracking algorithm? - "All permutations", "all subsets", "all combinations". - "All ways to split a string", "all paths through a maze". - "Place N queens", "solve the sudoku". - "Find any valid arrangement" with n under 20. The shared signal: every answer, or one answer under hard constraints. And n is small. If the task only counts answers, dynamic programming usually fits better. ## How does the backtracking template work? - A function receives the current partial solution, path. - If path is complete, record a copy and return. - For each valid option: append it to path, recurse, then pop it. The three lines "append, recurse, pop" are the backtrack. Without the pop, path grows forever. ## What does one run look like? Permutations of [1, 2, 3]. Path is empty. Take 1: [1]. Take 2: [1, 2]. Take 3: [1, 2, 3], complete, recorded. Pop 3, no options left. Pop 2. Take 3: [1, 3]. Take 2: [1, 3, 2], recorded. Back to empty. Take 2 first: [2, 1, 3], [2, 3, 1]. Then [3, 1, 2], [3, 2, 1]. Six answers, none missed. ## How do you write a backtracking algorithm in Python? ``` def permutations(nums): result, path, used = [], [], [False] * len(nums) def go(): if len(path) == len(nums): result.append(path[:]) # a copy, not a reference return for i, x in enumerate(nums): if used[i]: continue used[i] = True path.append(x) go() path.pop() # backtrack used[i] = False go() return result def subsets(nums): result, path = [], [] def go(start): result.append(path[:]) for i in range(start, len(nums)): path.append(nums[i]) go(i + 1) path.pop() go(0) return result ``` The start parameter in subsets forbids going back. That is why [1, 2] and [2, 1] are not counted twice. ## How does backtracking compare with its neighbours? | Pattern | Gives you | Size of n | Example | | --- | --- | --- | --- | | Backtracking | All solutions, or one under constraints | Up to 15-20 | N queens, permutations | | Dynamic programming | A count or an optimum | Thousands | Number of paths, knapsack | | Depth-first search | Reachability, components | Millions | Islands in a grid | | Greedy | An optimum without search | Any | Interval scheduling | ## How does backtracking solve N Queens? Place one queen per row. For row r, try every column c. Skip it if the column or a diagonal is taken. Keep three sets: used columns, diagonals r - c, and diagonals r + c. The check costs O(1). Reaching row n means a full board, record it. For n = 8, brute force has 8 to the power 8 boards. Pruning leaves a few thousand. ## What mistakes break a backtracking algorithm? - Recording path instead of path[:]. Every answer becomes the same empty list. - Forgetting the pop after the recursive call. - Checking constraints only at the end instead of every step. Pruning never happens. - Not sorting the input when it has duplicates, so equal answers repeat. ## How do you explain backtracking in an interview? Say: I need every solution and n is small. I build the answer step by step and undo. Name the three template lines. Say where you prune. State the complexity honestly: O(n!) for permutations, O(2ⁿ) for subsets. Then say pruning cuts the real step count a lot. Drill this pattern on the [backtracking](/patterns/backtracking) page. The base skill lives on the [recursion](/patterns/recursion) page. ## FAQ ### How is backtracking different from depth-first search? The mechanics match. Backtracking builds a solution and undoes a step. DFS just marks nodes as visited. ### How do you remove duplicates in subsets with repeated values? Sort the input. In the loop, skip an element equal to the previous one. Do that only when the previous one was skipped at this level. ### Where does backtracking show up in real systems? Constraint solvers: timetables, assigning jobs to machines. Parsers that try one grammar rule, fail, and try another. Game engines that search moves a few steps ahead. ### When is backtracking too slow? When n is above 20 and pruning is weak. Then look for dynamic programming or a greedy rule. --- title: "Prefix sum array: range sums in O(1) and counting subarrays with a hash map" url: https://algopath.pro/blog/prefix-sum-array language: en summary: "One pass, then any range sum costs O(1). How it is built, Python code, the hash map trick for Subarray Sum Equals K, the 2D version and the mistakes that break it." updated: 2026-10-16 --- # Prefix sum array: range sums in O(1) and counting subarrays with a hash map **Short answer.** A prefix sum array stores the sum of the first i elements in pref[i]. After one O(n) pass, any range sum costs O(1): pref[r + 1] - pref[l]. Use it when sum queries are many and the array does not change. With a hash map it also counts subarrays with a given sum. ## What is a prefix sum? Take an array a. Build an array pref of length n + 1. pref[0] is 0, and pref[i] is the sum of a[0] .. a[i - 1]. The sum of a[l..r] is pref[r + 1] - pref[l]. Everything before l cancels. What remains is exactly the range. Building costs O(n). Each query after that costs O(1). Without prefixes, each query costs O(n). ## When does a problem need prefix sums? - "Many queries of the form sum of a range." - "How many subarrays sum to k." - "Find the index where the left sum equals the right sum." - "Longest subarray with sum 0", or with equal zeros and ones. The shared signal: many range sums over a fixed array. If the array changes between queries, reach for a Fenwick tree instead. ## What does a prefix sum look like on an example? Array [2, 4, 1, 3]. pref = [0, 2, 6, 7, 10]. Sum of indexes 1 to 2, which is 4 + 1. pref[3] - pref[1] = 7 - 2 = 5. Whole array: pref[4] - pref[0] = 10. Empty range: pref[2] - pref[2] = 0. The leading zero removes the special case for l = 0. ## How do you write prefix sums in Python? ``` from itertools import accumulate def build(a): pref = [0] for x in a: pref.append(pref[-1] + x) return pref # or list(accumulate(a, initial=0)) def range_sum(pref, l, r): return pref[r + 1] - pref[l] # sum of a[l..r] def subarray_sum_equals_k(nums, k): seen = {0: 1} # prefix 0 seen once total = count = 0 for x in nums: total += x count += seen.get(total - k, 0) seen[total] = seen.get(total, 0) + 1 return count ``` The third function never stores pref. It only needs the running total and a count of earlier totals. ## How does a prefix sum solve Subarray Sum Equals K? Count subarrays with sum k. The subarray l..r has sum pref[r + 1] - pref[l]. That equals k when pref[l] equals pref[r + 1] - k. Walk left to right. At each step, count how often the prefix total - k appeared before. That many subarrays end here. Then record the current prefix. The entry {0: 1} counts subarrays that start at index 0. Without it those go missing. ## How do prefix sums compare with the neighbouring patterns? | Pattern | Array changes | Query | Negative numbers | | --- | --- | --- | --- | | Prefix sums | No | O(1) | Yes | | Sliding window | No | O(n) for the whole pass | No, the condition stops being monotonic | | Fenwick tree | Yes | O(log n) | Yes | | Difference array | Many range additions | O(1) per update | Yes | ## How do you extend the idea to a matrix? A 2D prefix pref[i][j] holds the rectangle sum from (0, 0) to (i - 1, j - 1). It builds in O(n · m). A rectangle sum uses inclusion and exclusion. Subtract the rectangle above and the one to the left. Add back their overlap, which was subtracted twice. Four reads, O(1). ## What mistakes break a prefix sum solution? - A pref of length n instead of n + 1. Sums from index 0 need a special case. - Recording the current prefix before the lookup. Empty subarrays start counting. - Forgetting {0: 1} and losing subarrays that start at the front. - Using prefixes on an array that changes. Each change costs an O(n) rebuild. ## How do you explain prefix sums in an interview? Say: many range sum queries, fixed array. Name pref and the subtraction formula. Give O(n) to build and O(1) per query. If the task counts subarrays, add the hash map. Say: "I look for the prefix total - k among earlier ones." That sentence is the key. Drill this pattern on the [prefix sums](/patterns/prefix-sums) page. The neighbouring trick lives on the [difference array](/patterns/difference-array) page. ## FAQ ### Do prefix sums work for products or XOR? Yes, for any operation with an inverse. XOR is its own inverse. Products break on zeros, which need separate handling. ### How do you find the index where left sum equals right sum? Compute the total once. Walk the array with a running left sum. The right sum is total minus left minus the current element. The first match is the answer. ### What if the array changes? Rebuilding after every change is too slow. Use a Fenwick tree: O(log n) per update and per query. ### Where do prefix sums show up in real systems? Analytics: revenue over any period from running totals. Graphics: integral images for fast blur. Databases: range aggregates over sorted data. --- title: "Sliding window algorithm: the template, Python code and how to spot it in a problem" url: https://algopath.pro/blog/sliding-window-algorithm language: en summary: "Two indexes that only move right turn longest-substring and smallest-subarray problems into O(n). Trigger words, the template, Python code, a traced run and Minimum Window Substring." updated: 2026-10-15 --- # Sliding window algorithm: the template, Python code and how to spot it in a problem **Short answer.** The sliding window algorithm keeps two indexes, left and right. Both only move forward. The stretch between them is the window. The right edge grows it. The left edge shrinks it when a condition breaks. Longest or shortest contiguous stretch problems drop from O(n²) to O(n). Below: trigger words, the template, Python code and a traced run. ## What is the sliding window algorithm? A window is a contiguous slice of an array or string. Two indexes define it. Everything between left and right is inside. A fixed window has a set length and just slides right. A variable window grows and shrinks by a rule. The variable kind shows up more often in interviews. Both indexes only move right. Each element enters the window once and leaves once. That is why the whole pass costs O(n). ## When does a problem need a sliding window? - "Longest substring without repeating characters." - "Smallest subarray with sum at least k." - "Maximum sum of k consecutive elements." - "Does string t contain a permutation of string s." The shared signal: a contiguous stretch with a cheap property. Moving an edge updates it in O(1). If elements need not be adjacent, a window is the wrong tool. ## How does the sliding window template work? - Set left = 0. Move right from 0 to n - 1. - Add element right to the running state. - While the condition is broken, remove element left and move left forward. - The window is valid now. Update the answer with its length or content. The inner while loop does not make it quadratic. Left walks the array once over the whole run. ## What does one run look like? String "abcabcbb", longest substring without repeats. Right reaches a, b, c: window "abc", length 3. Right hits the second a: a repeat. Move left until the first a leaves. Window "bca". Right hits the second b: left moves past the first b. Window "cab". Right hits the second c: window "abc". Then "cb", then "b". Answer 3. ## How do you write a sliding window in Python? ``` def longest_unique(s): seen = set() left = best = 0 for right, ch in enumerate(s): while ch in seen: # condition broken seen.remove(s[left]) left += 1 seen.add(ch) best = max(best, right - left + 1) return best def min_subarray_len(nums, target): left = total = 0 best = float("inf") for right, x in enumerate(nums): total += x while total >= target: # window valid, try to shrink best = min(best, right - left + 1) total -= nums[left] left += 1 return 0 if best == float("inf") else best ``` The second function shrinks while the window is valid. That finds the shortest one. Same skeleton, flipped condition. ## How is a sliding window different from two pointers? | Trait | Sliding window | Two pointers, opposite ends | | --- | --- | --- | | Direction | Both move right | Move towards each other | | Looking for | A contiguous stretch | A pair of elements | | Needs sorting | No | Usually yes | | State | A counter over the window | Two values | | Example | Longest Substring Without Repeating | Two Sum II | Three more pairs people confuse, and how to log them, are in [two pointers vs sliding window](/blog/two-pointers-vs-sliding-window). ## How does the sliding window solve Minimum Window Substring? Given strings s and t. Find the shortest window of s holding every letter of t. Counts included. Count the letters of t in a dictionary called need. Move right and decrement need for each letter. Keep a counter called missing. When missing hits zero the window is valid. Shrink from the left while it stays valid, and record the best. Each letter enters and leaves once. Time O(|s| + |t|). ## What mistakes break a sliding window? - Resetting left to zero on a violation instead of stepping it. That is O(n²). - Updating the answer before the window is valid again. - Computing the length as right - left instead of right - left + 1. - Using a window for a subsequence problem, where elements can be apart. ## How do you explain the sliding window in an interview? Say: the answer is a contiguous stretch. Its property updates in O(1) per edge move. Name the two indexes and the state. Give O(n) and the reason: each index only moves right. Then show the O(n²) brute force. Say the window removes the recomputation. That is the whole argument. Drill this pattern on the [sliding window](/patterns/sliding-window-variable) page. The fixed-size version lives on the [fixed window](/patterns/sliding-window-fixed) page. ## FAQ ### Does a sliding window work with negative numbers? Not for sum problems. The condition "sum at least k" stops being monotonic. Use prefix sums with a hash map instead. ### How do you store the state for strings? A dictionary from letter to count. Or an array of 26 slots for lowercase letters. The array is faster, the dictionary is more general. ### How do you spot a fixed window? The statement gives a number k and says "consecutive". Then the window is always k long. Left equals right - k + 1. --- title: "Time complexity from the constraints" url: https://algopath.pro/blog/time-complexity-from-constraints language: en summary: "The line with the input size usually decides which algorithm you are allowed. A table you can memorise in five minutes, and what to do when nobody gives you one." updated: 2026-10-15 --- # Time complexity from the constraints **Short answer.** Read the input size first, because it tells you which complexity is allowed. A computer does about 100 million simple operations per second. So n up to 20 allows every subset, n up to 5,000 allows n², n up to 100,000 needs n log n, and n near a billion needs a log or a formula. Everything below follows from that one number, and from what [time complexity](https://en.wikipedia.org/wiki/Time_complexity) means. ## The table - **n up to 10**: anything. Try every permutation, that is 3.6 million. - **n up to 20**: every subset is a million. [Bitmask](https://en.wikipedia.org/wiki/Mask_(computing)) solutions live here and almost nowhere else. - **n up to 500**: n cubed is fine. [Floyd-Warshall](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm), interval dynamic programming. - **n up to 5000**: n squared is fine. Every pair, most string DP. - **n up to 100000**: you need n log n. Sorting, heaps, binary search inside a loop, balanced trees. - **n up to 10 million**: one pass, and keep it cheap. Windows, prefix sums, counting. - **n around a billion**: you cannot touch every element. Expect a log, a formula, or [binary search](https://en.wikipedia.org/wiki/Binary_search) on the answer. Five minutes of memorising. It removes most wrong turns before they happen. The last row: [binary search on the answer](/patterns/binary-search-on-answer). ## Read it backwards Go from the bound to the shape of the code. n log n usually means one sort. Or one loop with a binary search inside. Two nested passes over 100 thousand items fail before you write them. To check a function you already wrote, the [Big O calculator](/big-o-calculator) measures it. Forwards, you write the quadratic version and find out at submit time. Backwards, you skip that half hour. ## The other numbers Input size gets the attention. Three more lines carry as much. **The value range.** "Values up to 10 to the sixth" means an array of that size and direct counting. "Up to 10 to the ninth" means you cannot. Now it is a hash map or coordinate compression. **The alphabet.** "Lowercase English letters" means 26 slots. A fixed array replaces the hash map and the constant drops. A fixed array also makes the space O(1). Worth saying out loud. **The memory limit.** 256 MB is about 64 million 4-byte integers. A table of 100 thousand by 100 thousand will not fit. However elegant the recurrence is. ## Interviews rarely give you any of this Competitive problems state the bounds. Interviewers usually do not. Asking about constraints is one of the best questions to ask. "How large can this array be?" "Can the values be negative?" Four seconds each. Both change the answer. Asking shows the reflex being scored. Being told it does not matter still needs a stated assumption. Say what you are assuming and continue. Your solution is correct under a stated assumption. That beats being quietly wrong. One of five ideas in [why algorithm problems are hard](/blog/why-are-algorithms-so-hard). ## When the bound looks strange An odd constraint is a hint. Someone left it for you. n up to 20 with an unlimited value range is a bitmask problem announcing itself. A tiny n next to many queries means precompute once, answer fast. Exactly 10 to the fifth next to a 1 second limit means n log n. Nothing slower. Those numbers are chosen deliberately. Reading them is free information. ## Where this is drilled [AlgoPath](/) puts constraint reading in step 09, ["Big-O: a name for how fast the work grows"](/steps/big-o-and-constraints), before the first pattern. Every recognition prompt after it carries the bounds, for this reason. The first 33 steps are free. The notation behind the table is in [Big O explained simply](/blog/big-o-notation-explained-simply). --- title: "Dijkstra's algorithm in Python: shortest paths with a heap in O((V + E) log V)" url: https://algopath.pro/blog/dijkstra-algorithm-python language: en summary: "How Dijkstra finds shortest paths in a weighted graph, why the greedy pick is safe, Python code with heapq, a traced run, and Network Delay Time." updated: 2026-10-15 --- # Dijkstra's algorithm in Python: shortest paths with a heap in O((V + E) log V) **Short answer.** Dijkstra's algorithm finds the shortest path from one node to every other node. Edge weights must be non-negative. It pops the closest open node from a heap and relaxes its neighbours. Time O((V + E) log V). Below: the trigger words, Python code, a traced run and Network Delay Time. ## What does Dijkstra's algorithm do? You have a graph where every edge has a weight. You want the cheapest route from the start to each other node. Breadth-first search counts edges. It only works when every edge costs the same. Dijkstra handles weights, as long as none is negative. The idea: always take the closest node not yet settled. Its distance cannot improve any more. Use it to improve its neighbours. That improvement step is called relaxing an edge. ## When does a problem need Dijkstra's algorithm? - "Shortest path", "minimum cost", "minimum time". - "Weighted graph" with non-negative weights. - "Grid where each cell has an entry cost." - "How long until a signal reaches every node." All weights equal to one: use BFS. Negative weights: use Bellman-Ford. Dijkstra sits between the two. ## How does the algorithm work step by step? - Distance to the start is 0, to everything else infinity. - Push (0, start) onto a min-heap. - Pop the pair with the smallest distance. If it is larger than the known distance, skip it. - For each neighbour compute a new distance. If it is smaller than the stored one, store it and push. - Heap empty: all distances are final. The skip step is lazy deletion. Stale pairs stay in the heap and get ignored when they surface. ## What does one run of Dijkstra's algorithm look like? Nodes A, B, C, D. Edges: A-B weight 1, A-C weight 4. Also B-C weight 2 and C-D weight 1. Start at A. Pop A at 0. B becomes 1, C becomes 4. Pop B at 1. Through B, C becomes 3, better than 4, update. Pop C at 3. D becomes 4. Pop the stale (4, C) and skip it. Pop D. Result: A 0, B 1, C 3, D 4. ## How do you write Dijkstra's algorithm in Python? ``` import heapq def dijkstra(graph, start): # graph: {node: [(neighbour, weight), ...]} dist = {v: float("inf") for v in graph} dist[start] = 0 heap = [(0, start)] while heap: d, v = heapq.heappop(heap) if d > dist[v]: continue # stale entry for u, w in graph[v]: nd = d + w if nd < dist[u]: dist[u] = nd heapq.heappush(heap, (nd, u)) return dist ``` To recover the path itself, keep a prev dictionary. On every improvement set prev[u] = v. Then walk back from the target. ## How does Dijkstra compare with its neighbours? | Algorithm | Weights | Time | Use when | | --- | --- | --- | --- | | BFS | All equal | O(V + E) | Step counts, mazes | | Dijkstra | Non-negative | O((V + E) log V) | Roads, delays, prices | | Bellman-Ford | Any | O(V · E) | Negative edges exist | | Floyd-Warshall | Any | O(V³) | All pairs, small graph | ## How does it solve Network Delay Time? You get n nodes, a source, and edges as (from, to, time). Return the time for a signal to reach every node. Build an adjacency list, run Dijkstra from the source. The answer is the largest distance. If any distance is still infinity, return -1. ## What mistakes break a Dijkstra solution? - Running it on negative weights. The answer is silently wrong. - Not skipping stale pairs. Still correct, just slower. - Pushing (node, distance) instead of (distance, node). The heap sorts on the first field. - Marking a node settled on push instead of on pop. ## How do you explain Dijkstra's algorithm in an interview? Say: weighted graph, non-negative weights, single source. Name the heap and the greedy pick of the closest node. Give O((V + E) log V). Then justify the greed. Weights are non-negative. No detour can reach the closest node more cheaply. That is the follow-up question almost every time. Drill the pattern against its neighbours on the [Dijkstra's algorithm](/patterns/dijkstra) page. The unweighted case is on the [graph BFS / DFS](/patterns/graph-bfs-dfs) page. ## FAQ ### Why does Dijkstra fail with negative weights? A settled node is treated as final. A negative edge could later shorten the path to it. The algorithm never looks again. ### Where does Dijkstra show up in real systems? Map navigation: junctions are nodes, roads are edges, weights are travel times. Network routing protocols such as OSPF run it on every router. Any planner where an action has a cost. ### Do I need a visited set? No. The check d > dist[v] does the same job. A set is fine for clarity, the result is identical. ### How do you run it on a grid with cell costs? Each cell is a node, its four neighbours are edges. The weight is the cost of the cell you enter. Same code. --- title: "Priority queue in Python: heapq, the size-k trick and when a problem needs one" url: https://algopath.pro/blog/priority-queue-python language: en summary: "heapq turns a list into a min-heap with O(log n) push and pop. The API table, the max-heap trick, tuples for priorities, Top K Frequent Elements and Merge k Sorted Lists." updated: 2026-10-15 --- # Priority queue in Python: heapq, the size-k trick and when a problem needs one **Short answer.** In Python, a priority queue is the heapq module over a plain list. heappush and heappop cost O(log n), reading the minimum costs O(1). It is a min-heap. For a max-heap you push negated values. Use it when a problem asks for "k largest", "k closest" or "merge k sorted lists". ## What is a priority queue? A queue gives you the oldest item. A priority queue gives you the item with the best priority. Best means smallest by default. Under the hood it is a binary heap. That is a complete binary tree stored in an array. The root holds the minimum. Children of index i sit at 2i + 1 and 2i + 2. Insert puts the item at the end and bubbles it up. Pop moves the last item to the root and sinks it down. Both walk one path of height log n. ## How does the priority queue API in Python look? | Call | Cost | What it does | | --- | --- | --- | | heapq.heapify(lst) | O(n) | Turns a list into a heap in place | | heapq.heappush(h, x) | O(log n) | Adds x | | heapq.heappop(h) | O(log n) | Removes and returns the smallest | | h[0] | O(1) | Peeks at the smallest | | heapq.heappushpop(h, x) | O(log n) | Push then pop, faster than two calls | | heapq.nlargest(k, it) | O(n log k) | k largest items of any iterable | There is no decrease-key. To update a priority, push a new entry. Skip stale ones when they pop. This is called lazy deletion. ## When does a problem need a priority queue? - "k largest", "k smallest", "k most frequent", "k closest points". - "Merge k sorted lists" or "smallest range covering k lists". - "Median of a data stream" with two heaps. - "Task scheduler", "meeting rooms", "next event by time". The shared signal: items keep arriving, and you always need the best one. Sorting after every change costs O(n log n). The heap does it in O(log n). ## How do you write a priority queue in Python? ``` import heapq from collections import Counter # max-heap through negation h = [] for x in [5, 1, 8]: heapq.heappush(h, -x) largest = -heapq.heappop(h) # 8 # tuples: compared by the first field tasks = [] heapq.heappush(tasks, (2, "write tests")) heapq.heappush(tasks, (1, "fix build")) priority, name = heapq.heappop(tasks) # (1, "fix build") def top_k_frequent(nums, k): count = Counter(nums) h = [] for value, freq in count.items(): heapq.heappush(h, (freq, value)) if len(h) > k: heapq.heappop(h) # drop the rarest return [value for freq, value in h] ``` The heap of size k is the standard trick. It keeps the k best seen so far. Memory O(k), time O(n log k). ## What does one push look like? Heap [1, 3, 2, 7, 5]. Push 0. It goes to the end: [1, 3, 2, 7, 5, 0]. Index 5, parent index 2, value 2. Zero is smaller, swap: [1, 3, 0, 7, 5, 2]. Now index 2, parent index 0, value 1. Zero is smaller, swap: [0, 3, 1, 7, 5, 2]. Two swaps, done. Pop reverses the moves. The last item goes to the root and sinks. ## How does a priority queue solve Merge k Sorted Lists? Push the head of every list as (value, list index, node). Pop the smallest and append it to the output. Push the next node of that list. The heap never holds more than k nodes. Every node enters and leaves once. Total time O(N log k), where N counts all nodes. The list index in the tuple breaks ties. Without it Python tries to compare two nodes and raises. ## What mistakes break a priority queue solution? - Treating the heap list as sorted. Only index 0 is guaranteed. - Forgetting the minus sign and getting the k smallest. - Pushing objects that cannot be compared. Wrap them in a tuple with a number first. - Searching the heap linearly and calling it O(log n). ## How do you explain a priority queue in an interview? Say you need the current best while items keep changing. Give the costs: O(1) peek, O(log n) push and pop, O(n) build. Show the size-k trick. Then name the alternative: sort every time at O(n log n). That comparison is the reason for the heap. Drill the pattern against its neighbours on the [binary heap](/patterns/binary-heap) page. The full map of patterns lives in [LeetCode patterns](/blog/leetcode-patterns). ## FAQ ### Is queue.PriorityQueue better than heapq? It is thread safe and slower. For interviews and single threaded code, heapq is the right choice. ### How do you remove an arbitrary item? You cannot in O(log n) with heapq alone. Mark it as removed in a set. When it reaches the top, pop and ignore it. ### How do you build a max-heap without negation? Wrap items in a small class with a reversed __lt__. Or use heapq.nlargest when you need the k largest once. ### Why is heapify O(n) and not O(n log n)? Half the nodes are leaves and need no sinking. Higher levels have fewer nodes. The sum of all sinks is linear. --- title: "Binary search on the answer: the pattern behind minimum speed and smallest capacity" url: https://algopath.pro/blog/binary-search-on-answer language: en summary: "When you cannot compute the answer but can verify a guess, search the range of answers. The check function, the bounds, Python code and Koko Eating Bananas traced by hand." updated: 2026-10-15 --- # Binary search on the answer: the pattern behind minimum speed and smallest capacity **Short answer.** Binary search on the answer searches the range of possible answers. The input array stays unsorted. You guess a value and check it in O(n). Then you halve the range. It fits problems that say "minimum speed", "smallest capacity" or "maximum minimum distance". Below: the shape, the check function, Python code and Koko Eating Bananas. ## What is binary search on the answer? Classic binary search looks for a value in a sorted array. Here nothing is sorted. Instead the answers themselves form a range from low to high. Every candidate answer allows a yes or no question: is this enough? The answers to that question are monotonic. Below some threshold all answers are "no", above it all are "yes". Binary search finds the threshold. The method needs three things. A lower bound, an upper bound and a check function. ## When does a problem need binary search on the answer? - "Minimum speed to finish in h hours." - "Smallest ship capacity to ship all packages in d days." - "Split the array into k parts to minimise the largest sum." - "Place k cows in stalls to maximise the minimum distance." The signal is the word "minimum" or "maximum" next to a condition. Checking a fixed value is easy. Finding the best value directly is hard. That gap is what the pattern fills. ## How does the check function work? The check takes a candidate. It returns True if the candidate satisfies the task. It must be monotonic. Once True, it stays True for every larger candidate. Or the reverse, for maximising problems. Take Koko Eating Bananas. Candidate speed k. Hours needed is the sum of ceil(pile / k) over all piles. Return True when that sum is at most h. Faster speed, fewer hours. Monotonic. | Problem | Range | Check(x) | Want | | --- | --- | --- | --- | | Koko Eating Bananas | 1 .. max(piles) | hours at speed x <= h | smallest True | | Capacity to Ship Packages | max(w) .. sum(w) | days at capacity x <= d | smallest True | | Split Array Largest Sum | max(a) .. sum(a) | parts with limit x <= k | smallest True | | Aggressive Cows | 1 .. max gap | can place k cows at distance x | largest True | ## How do you write binary search on the answer in Python? ``` import math def min_eating_speed(piles, h): lo, hi = 1, max(piles) def ok(k): return sum(math.ceil(p / k) for p in piles) <= h while lo < hi: mid = (lo + hi) // 2 if ok(mid): hi = mid # mid works, try smaller else: lo = mid + 1 # mid fails, need bigger return lo # maximising version: largest x with ok(x) True def largest_ok(lo, hi, ok): while lo < hi: mid = (lo + hi + 1) // 2 if ok(mid): lo = mid else: hi = mid - 1 return lo ``` The loop keeps lo as the smallest value that could still work. When lo meets hi, that is the answer. The +1 in the maximising version stops an infinite loop. ## What does one run look like? Piles [3, 6, 7, 11], h = 8. Range 1 to 11. Try 6: hours are 1 + 1 + 2 + 2 = 6. Fine, so hi = 6. Try 3: hours are 1 + 2 + 3 + 4 = 10. Too many, so lo = 4. Try 5: hours are 1 + 2 + 2 + 3 = 8, fine, hi = 5. Try 4: hours are 1 + 2 + 2 + 3 = 8, fine, hi = 4. Now lo equals hi. Answer 4. Four checks instead of eleven. With a range up to a billion, it is thirty checks. ## How do you pick the bounds? The lower bound is the smallest value that makes sense. For a speed that is 1. For a capacity it is the largest single item, or nothing ships. The upper bound is a value that surely works. The largest pile, the sum of all weights. A loose upper bound costs a few extra checks. Err on the large side. ## What mistakes break binary search on the answer? - A check that is not monotonic. Then the halving throws away the answer. - Bounds that exclude the answer. Capacity below the largest item never ships it. - Using mid = (lo + hi) // 2 while maximising. lo = mid then never moves. - Integer division in the check where ceiling was needed. ## How do you explain binary search on the answer in an interview? Say: "I cannot compute the answer directly, but I can verify a guess in O(n)." Then say the verification is monotonic. Then name the bounds. Then write the loop. State the complexity as O(n log(hi - lo)). The log is over the answer range, not the array. Interviewers check that you know which log it is. Practise spotting this pattern on the [binary search on the answer](/patterns/binary-search-on-answer) page. The array version lives on the [binary search](/patterns/binary-search-array) page. ## FAQ ### How is this different from ordinary binary search? Ordinary binary search compares mid with a target inside a sorted array. This one calls a check function on mid. The array does not need to be sorted at all. ### Can the answer be a floating point number? Yes. Loop a fixed number of times, around 100. Do not wait for lo to equal hi. Or stop when hi - lo is below the required precision. ### What if the check is O(n log n)? Then the total is O(n log n log R). Usually that still passes. Sorting once outside the loop often brings the check back to O(n). --- title: "Big O notation explained simply" url: https://algopath.pro/blog/big-o-notation-explained-simply language: en summary: "Big O answers one question: if the input doubles, what happens to the work? Five shapes, and how to read them off your own loops." updated: 2026-10-15 --- # Big O notation explained simply **Short answer.** Big O notation answers one question. If the input doubles, what happens to the work? With O(1) it stays the same, with O(log n) it grows by one step, with O(n) it doubles, and with O(n²) it grows four times. Constants are dropped because only the growth rate matters on large inputs. Big O captures exactly that question. Everything else is [notation](https://en.wikipedia.org/wiki/Big_O_notation). ## Five shapes Doubling the input changes the running time in predictable ways. - **O(1)**: the work does not change. An array index. A hash map lookup. - **O(log n)**: the work goes up by one step. A million items takes 20 steps. Two million takes 21. - **O(n)**: the work doubles. One pass over everything. - **O(n log n)**: the work slightly more than doubles. Any decent sort. - **O(n²)**: the work goes up four times. Every pair. Five lines. They cover almost everything you will meet before graduate-level work. Which of them the input size allows is in [time complexity from the constraints](/blog/time-complexity-from-constraints). Every term used here is defined in the [glossary](/glossary). A fuller table per data structure is the [Big-O cheat sheet](https://www.bigocheatsheet.com/). ## Why constants get dropped A loop doing three things per element is O(n). So is a loop doing one thing. That feels wrong. Three times slower is three times slower. Big O answers a different question. How does this behave as the input grows without bound? A constant of three stays three forever. Going from n to n squared runs away. At a million, that is 3 million steps against a trillion. The constant stops being interesting. ## Reading it off your own code Three rules get you most of the way. **A loop over the input is n.** A loop inside a loop is n squared. Nesting multiplies. **Halving is log n.** Each pass throws away half of what is left. Binary search is the clearest example of halving. It is also why balanced trees are fast. **Sequential steps add, and addition keeps the larger.** Sort plus one pass is n log n plus n. That is n log n. The trap is a hidden loop. A method that scans a string inside your loop makes it quadratic. No second set of braces on screen. Call something inside a loop, ask what it costs. ## Space works the same way Same question, applied to memory. Double the input, how much more do you store? A few variables is O(1). A copy of the input is O(n). A table of every pair is O(n²). That last one runs out of memory first in practice. Interviewers ask for both. Answer only time, and you drop a point you had earned. Saying the space cost out loud is [thinking out loud](/blog/how-to-talk-during-a-coding-interview). ## Where the notation lies Two honest caveats. At small sizes the constants win. [Insertion sort](https://en.wikipedia.org/wiki/Insertion_sort) beats [quicksort](https://en.wikipedia.org/wiki/Quicksort) under about 20 elements. Real implementations switch to it at the bottom. Memory layout matters too. Walking an array beats walking a linked list of the same length. Both are O(n). One sits in [cache](https://en.wikipedia.org/wiki/CPU_cache). The other does not. Big O picks the algorithm. Measurement picks the implementation. You need both. The [Big O calculator](/big-o-calculator) times your own function at growing sizes and names its class. ## Where this sits in the track [AlgoPath](/) puts complexity in step 08, ["Why some ways are faster"](/steps/why-some-ways-are-faster), before any pattern. Every later step states its cost and expects you to read it. Why this is one of five missing pieces is in [why algorithm problems are hard](/blog/why-are-algorithms-so-hard). The first 33 steps are free. --- title: "Monotonic stack in Python: next greater element in O(n) and the loop to memorise" url: https://algopath.pro/blog/monotonic-stack-python language: en summary: "A stack that stays sorted answers next greater and next smaller for every index in one pass. Trigger words, the loop, Python code, the direction table and Largest Rectangle in Histogram." updated: 2026-10-15 --- # Monotonic stack in Python: next greater element in O(n) and the loop to memorise **Short answer.** A monotonic stack is a stack that stays sorted from bottom to top. It finds the next greater or next smaller element for every index. One pass, O(n). The trigger words are "next greater", "previous smaller", "how many days until" and "largest rectangle". Below: the loop, Python code, and its two most common problems. ## What is a monotonic stack? A normal stack lets you push anything. A monotonic stack pops first. Before pushing a value, you pop everything that would break the order. Keep it decreasing. The top is then the closest larger value. Keep it increasing, and the top is the closest smaller value. Each element is pushed once and popped at most once. So the whole pass is O(n), nested while loop included. ## When does a problem need a monotonic stack? - "Next greater element" or "next smaller element" for each index. - "How many days until a warmer temperature." - "Largest rectangle in a histogram" and "trapping rain water". - "Remove k digits to make the smallest number." The shared shape: find the nearest neighbour that beats the current value. Nested loops give O(n²). The stack gives O(n). ## How does the monotonic stack loop work? - Walk the array left to right. The stack holds indexes, not values. - While the current value beats the value at the top, pop. The popped index has just found its answer: the current index. - Push the current index. - Indexes left on the stack at the end have no answer. Use -1. Storing indexes lets you compute distances and read values. Storing values loses the position. ## What does the monotonic stack look like on an example? Take [2, 1, 4, 3]. Push index 0 (value 2). Value 1 does not beat 2, push index 1. Stack holds 2, 1 from bottom to top. Value 4 arrives. It beats 1: pop index 1, its answer is 4. It beats 2: pop index 0, its answer is 4. Push index 2. Value 3 does not beat 4, push index 3. End of input. Indexes 2 and 3 stay on the stack and get -1. Result: [4, 4, -1, -1]. Four pushes, two pops, no wasted work. ## How do you write a monotonic stack in Python? ``` def next_greater(nums): result = [-1] * len(nums) stack = [] # indexes, values decreasing for i, x in enumerate(nums): while stack and nums[stack[-1]] < x: result[stack.pop()] = x stack.append(i) return result def daily_temperatures(temps): answer = [0] * len(temps) stack = [] for i, t in enumerate(temps): while stack and temps[stack[-1]] < t: j = stack.pop() answer[j] = i - j stack.append(i) return answer ``` Change the comparison to > and you get next smaller. Walk right to left and you get previous greater or smaller. ## Which direction and which comparison does a monotonic stack need? | Question | Walk | Pop while top is | Stack order | | --- | --- | --- | --- | | Next greater | Left to right | smaller than current | Decreasing | | Next smaller | Left to right | larger than current | Increasing | | Previous greater | Left to right, read top before push | smaller than current | Decreasing | | Previous smaller | Left to right, read top before push | larger than current | Increasing | For "previous" questions, read the top after popping and before pushing. For "next" questions the answer is assigned to the popped index. ## How does the stack solve Largest Rectangle in Histogram? Each bar extends left and right until a shorter bar. The rectangle width is the gap between those two bars. Keep an increasing stack of indexes. When a shorter bar arrives, pop the taller one. Its right boundary is the current index. Its left boundary is the new stack top. Area is height times the width between them. Append a bar of height 0 to flush the stack. One pass, O(n). The brute force is O(n²). ## What mistakes break a monotonic stack? - Storing values instead of indexes, then needing a distance. - Using <= where < was needed. Equal values change which neighbour counts. - Forgetting to flush the stack at the end of a histogram problem. - Reading the answer at the wrong moment for "previous" questions. ## How do you explain a monotonic stack in an interview? Say the question is about the nearest bigger neighbour. Say a brute force costs O(n²). Then say: keep unanswered indexes on a decreasing stack. Each new value answers the ones it beats. Finish with the amortised argument. Every index enters once and leaves once, so the loop is linear. Interviewers wait for that sentence. Drill this pattern against its neighbours on the [monotonic stack](/patterns/monotonic-stack) page. The sliding window version is on the [monotonic deque](/patterns/monotonic-deque) page. ## FAQ ### What is the difference between a monotonic stack and a monotonic deque? A deque also removes from the front. So it can drop elements that left a window. A stack only handles the "nearest neighbour" family. ### Can a monotonic stack handle a circular array? Yes. Walk the indexes twice, from 0 to 2n - 1. Use i mod n. Only push during the first pass or push both, both work. ### Why is it O(n) with a while loop inside a for loop? The while loop pops. You cannot pop more times than you pushed. Total pushes are n, so total pops are at most n. --- title: "Merge sort vs quicksort: which one to write in an interview and why" url: https://algopath.pro/blog/merge-sort-vs-quicksort language: en summary: "Both split and conquer in O(n log n), but they differ on stability, memory and the worst case. Python code for both, the table that settles the choice, and the merge-sort counting trick." updated: 2026-10-15 --- # Merge sort vs quicksort: which one to write in an interview and why **Short answer.** Merge sort splits the array in half, sorts each half and merges. Quicksort picks a pivot and moves smaller values left, larger right. Then it sorts both sides. Both run in O(n log n) on average. Merge sort is stable and predictable; quicksort is faster in place. Interviewers ask you to write one and explain the other. ## How does merge sort work? Split the array into two halves. Sort each half the same way. Then merge two sorted halves in one pass with two indexes. The recursion depth is log n. Each level does O(n) work in the merge. That gives O(n log n) in every case, best and worst. The cost is memory. Merging needs a second array of size n. ## How does quicksort work? Pick a pivot value. Partition the array: smaller values go left of the pivot, larger go right. The pivot is now in its final place. Sort the two sides. Partition happens in place, so extra memory is only the recursion stack. With a random pivot the expected time is O(n log n). A bad pivot on every level gives O(n²). Sorted input with the first element as pivot does exactly that. ## Merge sort vs quicksort: which one should you pick? | Property | Merge sort | Quicksort | | --- | --- | --- | | Worst case | O(n log n) | O(n²), rare with a random pivot | | Extra memory | O(n) | O(log n) stack | | Stable | Yes | No | | Linked lists | Natural fit | Awkward | | Cache behaviour | Good | Better, in place | | Used by | Python sorted (Timsort), Java objects | C qsort, Java primitives | Pick merge sort when stability matters. Pick it for linked lists too. Pick quicksort when memory is tight and average speed matters. ## When does a problem need merge sort specifically? - "Count inversions" or "count smaller numbers after self". The merge step counts them for free. - "Sort a linked list in O(n log n)". Merge sort needs no random access. - "External sort" of data that does not fit in memory. - Any sort where equal keys must keep their input order. ## What does one merge step look like? Take the halves [2, 5, 9] and [1, 6]. Two indexes start at 0. Compare 2 and 1: take 1. Compare 2 and 6: take 2. Compare 5 and 6: take 5. Compare 9 and 6: take 6. The right half is empty now. Append the rest of the left half: 9. Result: [1, 2, 5, 6, 9]. Five comparisons at most, one for each element placed. Quicksort's partition on [5, 3, 8, 1, 9] with pivot 5 works differently. Walk from the left until a value at or above 5, walk from the right until a value at or below 5, swap. The result is [1, 3] on one side and [8, 9] on the other. The pivot sits between them. ## How do you write merge sort and quicksort in Python? ``` def merge_sort(a): if len(a) <= 1: return a mid = len(a) // 2 left, right = merge_sort(a[:mid]), merge_sort(a[mid:]) out, i, j = [], 0, 0 while i < len(left) and j < len(right): if left[i] <= right[j]: # <= keeps it stable out.append(left[i]); i += 1 else: out.append(right[j]); j += 1 return out + left[i:] + right[j:] import random def quick_sort(a, lo=0, hi=None): if hi is None: hi = len(a) - 1 if lo >= hi: return pivot = a[random.randint(lo, hi)] i, j = lo, hi while i <= j: while a[i] < pivot: i += 1 while a[j] > pivot: j -= 1 if i <= j: a[i], a[j] = a[j], a[i] i += 1; j -= 1 quick_sort(a, lo, j) quick_sort(a, i, hi) ``` The quicksort above uses Hoare style partition with a random pivot. It handles duplicates without going quadratic. ## How does merge sort solve Count of Smaller Numbers After Self? For each element, count how many elements to its right are smaller. The naive loop is O(n²). Run merge sort on pairs (value, original index). Now watch the merge. You take an element from the left half. Every element already taken from the right half is smaller. Add that count to the left element's answer. The counting rides on the merge. The total stays O(n log n). This is the most common "hard" question built on merge sort. ## What mistakes break these sorts in an interview? - Using < instead of <= in the merge. The sort stops being stable. - Taking the first element as the quicksort pivot. Sorted input turns quadratic. - Recursing on [lo, hi] instead of [lo, j] and [i, hi]. That loops forever. - Copying slices at every level in Python. Fine for an interview, slow at scale. ## How do you explain merge sort against quicksort in an interview? Start with the shared shape: divide and conquer, depth log n. Then name the one difference. Merge sort does the work after the split, quicksort does it before. Give the trade. Merge sort: stable, O(n) memory. Quicksort: in place, O(n²) worst case. Mention that Python's sorted is a merge sort variant. That shows you know what runs in production. Practise telling these two apart on the [fast sort](/patterns/fast-sort) page. The whole map of patterns lives in [LeetCode patterns](/blog/leetcode-patterns). ## FAQ ### Is quicksort always faster than merge sort? On arrays of primitives in memory, usually yes, by a constant factor. On linked lists or when stability is required, merge sort wins. ### What is Timsort? A merge sort that first finds runs already in order. Then it merges them. Python and Java use it for objects. Sorted or nearly sorted input runs in O(n). ### Do I need to write these from memory? Merge sort yes, in about 15 lines. For quicksort, interviewers mostly want the partition step and the pivot discussion. --- title: "Fast and slow pointers: cycle detection and the middle of a list in O(1) space" url: https://algopath.pro/blog/fast-and-slow-pointers language: en summary: "Two pointers at speeds one and two find a cycle, its start, and the middle node without extra memory. The wording that calls for it, Python code, the proof, and Find the Duplicate Number." updated: 2026-09-30 --- # Fast and slow pointers: cycle detection and the middle of a list in O(1) space **Short answer.** Fast and slow pointers walk the same list at different speeds. The slow one moves one step, the fast one moves two. If they ever meet, the list has a cycle. The same trick finds the middle of a list in one pass. Below: the trigger words, Python code, and the cycle start proof. ## What are fast and slow pointers? You keep two references into a linked list. Both start at the head. Each step, slow advances by one node and fast by two. Without a cycle, fast reaches the end first. With a cycle, fast laps slow. They land on the same node. The pattern is also called Floyd's tortoise and hare. It needs O(1) extra memory. That is its whole point. ## When does a problem need fast and slow pointers? - "Detect a cycle" in a linked list, without extra memory. - "Find the middle node" in one pass. - "Is this number happy" or any sequence that may loop. - "Find the duplicate number" where the array holds values in 1..n. The common signal is a sequence you can only walk forward. There is no index to jump to. A hash set works too, but it costs O(n) memory. Interviewers ask for the O(1) version. ## How do fast and slow pointers find the middle? Move slow by one and fast by two. Stop when fast hits the end. Slow now sits on the middle node. With an even length there are two middles. This code returns the second one. Stop fast one node earlier to get the first. ``` class Node: def __init__(self, val, next=None): self.val = val self.next = next def middle(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next return slow def has_cycle(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: return True return False def cycle_start(head): slow = fast = head while fast and fast.next: slow, fast = slow.next, fast.next.next if slow is fast: break else: return None slow = head while slow is not fast: slow, fast = slow.next, fast.next return slow ``` ## Why does the cycle start trick work? Call the distance from head to the cycle start a. Call the cycle length c. When the pointers meet, slow has walked a + k steps. Fast has walked twice that. The difference between them is a whole number of laps. So a + k is a multiple of c. Move one pointer back to the head. Walk both at speed one. After a steps, the pointer from the head reaches the start. The other one has walked a more steps inside the cycle. It also lands on the start. ## How do fast and slow pointers compare with a hash set? | Approach | Time | Memory | Finds cycle start | | --- | --- | --- | --- | | Hash set of visited nodes | O(n) | O(n) | Yes, first repeated node | | Fast and slow pointers | O(n) | O(1) | Yes, with the second phase | | Marking nodes in place | O(n) | O(1) | Yes, but it mutates input | Pick the hash set when memory is allowed. It is the shortest code. Pick two pointers when the interviewer says O(1) space. ## How does the pattern solve Find the Duplicate Number? An array of n + 1 integers holds values from 1 to n. One value repeats. You may not modify the array or use extra memory. Treat each value as a pointer: index i points to index nums[i]. Two indexes point to the same place. So the walk has a cycle. The duplicate is the cycle start. Run the cycle start code on this walk. ## What mistakes break fast and slow pointers? - Checking only fast and not fast.next before taking two steps. That raises on a list of odd length. - Comparing values with == instead of nodes with is. Two nodes can hold the same value. - Starting fast at head.next. The proof above assumes both start at head. - Forgetting the empty list. Return early when head is None. ## How do you explain fast and slow pointers in an interview? Name the constraint first: O(1) memory. Say the speeds: one and two. State the fact: in a cycle, fast catches slow within one lap. Then write the loop. If asked for the cycle start, say "second phase, reset one pointer to the head". Give the a + k argument in two sentences. That is enough. Drill the pattern on the [fast and slow pointers](/patterns/fast-slow-pointers) page. The full list of patterns is in [LeetCode patterns](/blog/leetcode-patterns). ## FAQ ### Why speed two and not three? Any speed above one works for detection. Two keeps the proof simple and the walk short. Speed three may take longer to meet inside a short cycle. It still meets. ### Does the pattern work on arrays? Yes, whenever a value can be read as the next index. Find the Duplicate Number is the classic case. ### Can fast and slow pointers find the k-th node from the end? That is a different two-pointer trick. Both move at speed one. A gap of k nodes sits between them. Same family, different rule. --- title: "Why are algorithms so hard when you can already code" url: https://algopath.pro/blog/why-are-algorithms-so-hard language: en summary: "Knowing a language and solving algorithm problems are two different skills. Five specific things sit between them, and none of them is talent." updated: 2026-10-15 --- # Why are algorithms so hard when you can already code **Short answer.** Writing code and solving algorithm problems are two skills, and the second rests on five ideas nobody lists. What an operation costs. Which container fits the question. The invariant a loop keeps. Solving a small example by hand. Reading the constraints as instructions. Each idea fits in a paragraph, and all five take about a week. ## 1. What an operation costs You know how to look something up in an array. The question is what that costs. An array index is one step whatever the size. Searching an unsorted array is one step per element. Looking up a key in an [associative array](https://en.wikipedia.org/wiki/Associative_array) is one step. Sorting is n log n. Five facts. Without them every choice is a coin flip. You cannot compare two plans you cannot price. How to price them is in [Big O notation explained simply](/blog/big-o-notation-explained-simply). ## 2. Choosing the container Most beginners keep everything in an array. Arrays are what they were taught. The question worth asking is what you need to do most often. Asking "have I seen this" points at a [set](https://en.wikipedia.org/wiki/Set_(abstract_data_type)). Asking "what value belongs to this key" points at a map. "Give me the smallest so far" points at a [heap](https://en.wikipedia.org/wiki/Heap_(data_structure)). Order matters, keep an array. Half of all interview problems are solved by picking right here. The rest of the code follows on its own. ## 3. The invariant An [invariant](https://en.wikipedia.org/wiki/Loop_invariant) is one sentence. It stays true on every pass of the loop. "Everything left of i is already sorted." "The window always sums to at most k." Loops without one are written by trial and error. You add a plus one, the test passes, and you never learn why. Write the sentence before the loop. Most off-by-one bugs stop existing. A plus one either keeps the sentence true or it does not. ## 4. Working the example by hand Take four elements and solve them on paper. Slowly. Your hand compared two things, remembered a best value, and moved forward. Comparing, remembering, and moving forward is the algorithm. Most techniques in the first half of any curriculum formalise something people already do by hand. Skip this step, and the code feels like it came from nowhere. ## 5. Reading constraints as instructions "n up to 1000" allows a quadratic solution. "n up to 200000" forbids it. One line often decides the whole approach. Beginners read it as trivia. The full table of bounds is in [time complexity from the constraints](/blog/time-complexity-from-constraints). ## What none of this is None of the five is about intelligence. None is about maths. The five are small pieces of knowledge, one paragraph each. Most tutorials assume you have them. Six months of writing working code is enough. All five fit in a week. The order it arrives in makes it feel impossible. You were handed step 40 and told it was step 5. ## Where the ladder starts [AlgoPath](/) starts before the first algorithm, with the free lesson [what is an algorithm](/steps/what-is-an-algorithm). Costs, containers, invariants, constraints. One step each, in that order, with code you write. The first 33 steps are free, and they cover all five. The skill above the five is [pattern recognition](/blog/leetcode-patterns). The mixed-up pairs are in [algorithm patterns people confuse](/blog/two-pointers-vs-sliding-window). If the material itself feels hostile rather than hard, that is [a different problem with a different fix](/blog/i-hate-algorithms). --- title: "Trie in Python: how a prefix tree works and when a problem needs one" url: https://algopath.pro/blog/trie-python language: en summary: "A trie stores words letter by letter and answers prefix questions in O(L). The Python class, how it differs from a hash set, the wording that calls for it, and Word Search II worked through." updated: 2026-10-15 --- # Trie in Python: how a prefix tree works and when a problem needs one **Short answer.** A trie is a tree. Every path down from the root spells a prefix. Each node holds its children by letter and a flag that says "a word ends here". Insert and lookup cost O(L) for a word of length L. The number of stored words does not matter. ## What is a trie? Take the words "car", "cat" and "cart". A trie stores them once, letter by letter, sharing what they share. - The root has one child, c. - c has one child, a. - a has two children, r and t. The t is marked as an end. - r is marked as an end. It has one child, t, also an end. Six nodes for three words. The prefix "ca" exists once, not three times. The other name is prefix tree. Same thing. ## How do you write a trie in Python? A dictionary per node keeps the code short. No fixed alphabet, no arrays of 26. ``` class Trie: def __init__(self): self.root = {} def insert(self, word): node = self.root for ch in word: node = node.setdefault(ch, {}) node["$"] = True # a word ends here def search(self, word): node = self._walk(word) return node is not None and "$" in node def starts_with(self, prefix): return self._walk(prefix) is not None def _walk(self, text): node = self.root for ch in text: if ch not in node: return None node = node[ch] return node ``` Three details carry the code. - The end marker is a key that no letter can be. `"$"` works; so does `None`. - `search` and `starts_with` share one walk. Only the last check differs. - Every method is O(L). The number of stored words never appears. ## When does a problem want a trie? The wording again. - "Autocomplete", "suggest words as the user types". - "Does any word start with this prefix" asked many times. - "Search a board for all words from a list". - "Longest common prefix" over many strings. - "Replace words with their shortest root". - Bits instead of letters: "maximum XOR of two numbers". The common thread: many strings and many prefix questions. A hash set answers only whole words. Signals, near misses and code templates are on the [trie pattern page](/patterns/trie). ## How is a trie different from a hash set? Both answer "is this word here". They part ways on prefixes. | | Trie | Hash set | | --- | --- | --- | | Whole-word lookup | O(L) | O(L) on average | | Prefix lookup | O(L) | Not supported | | All words with a prefix | Walk the subtree | Scan everything | | Memory | One node per distinct prefix | One entry per word | | Ordered output | Free, walk children in order | Sort first | No prefix questions in the problem? Use the set. A trie you do not need is a hundred lines of nothing. ## How do you solve Word Search II with a trie? The problem. A grid of letters and a list of words. Return every word that can be traced through adjacent cells. Each cell is used once per word. Searching the grid once per word is too slow. Put the words in a trie and search the grid once. Run a DFS from every cell. Carry the trie node along. A cell whose letter is not a child of the current node is a dead end. The search stops there. ``` def find_words(board, words): trie = Trie() for w in words: trie.insert(w) found = set() rows, cols = len(board), len(board[0]) def dfs(r, c, node, path): ch = board[r][c] if ch not in node: return nxt = node[ch] path += ch if "$" in nxt: found.add(path) board[r][c] = "#" for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)): nr, nc = r + dr, c + dc if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] != "#": dfs(nr, nc, nxt, path) board[r][c] = ch for r in range(rows): for c in range(cols): dfs(r, c, trie.root, "") return list(found) ``` The `"#"` marks a cell as used on the current path. It is put back on the way out. The pruning is the whole point. Without the trie, every path is followed to its full length. With it, most paths die on the second letter. ## How do you explain a trie in an interview? Three sentences before the code. - "There are many prefix questions, so a trie. A set only answers whole words." - "Each node maps a letter to a child and marks where a word ends." - "Insert and lookup are O(L). The word count does not matter." Then the class. Then the three-word example above. Say what the end marker is for. That is the part people skip. ## What goes wrong with a trie? - No end marker. "car" and "cart" become one word, and `search("car")` is wrong. - End marker as a letter key. A word containing that letter breaks the trie. - Rebuilding the trie per query. Build once, then ask. - A trie for a problem with no prefixes. Slower to write and no faster to run. Recognising the pattern from the wording is the harder half. That is what [LeetCode patterns](/blog/leetcode-patterns) is about. ## FAQ ### What is the time complexity of a trie? O(L) for insert, search and prefix lookup. L is the word's length. Building it is O(total letters). ### How much memory does a trie use? One node per distinct prefix. Many words with shared prefixes are cheap. Random strings are expensive. ### Trie or hash map for autocomplete? Trie. Autocomplete is a prefix question. A hash map cannot answer it without scanning. ### Can a trie store numbers? Yes, bit by bit. A binary trie over 32 bits solves "maximum XOR of two numbers" in O(32 n). --- title: "Union-Find in Python: what it is and when a problem needs it" url: https://algopath.pro/blog/union-find-python language: en summary: "Union-Find tracks which elements share a group and merges groups in near O(1). The Python class with path compression and union by size, the wording that calls for it, and the four mistakes that cost the most." updated: 2026-10-15 --- # Union-Find in Python: what it is and when a problem needs it **Short answer.** Union-Find is a structure that tracks which elements share a group. It does two things fast. Tell whether two elements are connected. Merge two groups. With two small tricks each operation is close to O(1). Below: the Python code, the wording that calls for it, and the costly mistakes. ## What is Union-Find? Every element points at a parent. Follow the parents up and you reach a root. Two elements are in the same group when they share a root. That is the whole idea. Two operations sit on top of it. - `find(x)` walks up from x and returns its root. - `union(a, b)` finds both roots and makes one point at the other. The other name is Disjoint Set Union, or DSU. Same structure, different label. ## How do you write Union-Find in Python? Elements are numbered 0 to n-1. Each starts as its own root. ``` class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.size = [1] * n def find(self, x): while self.parent[x] != x: self.parent[x] = self.parent[self.parent[x]] # path halving x = self.parent[x] return x def union(self, a, b): ra, rb = self.find(a), self.find(b) if ra == rb: return False if self.size[ra] < self.size[rb]: ra, rb = rb, ra self.parent[rb] = ra self.size[ra] += self.size[rb] return True ``` Two lines do the heavy lifting. - Path halving in `find`. Every node on the way up skips a step. Trees stay flat. - Union by size. The smaller tree hangs under the larger one, so trees stay shallow. With both, a sequence of m operations costs O(m α(n)). α grows so slowly that it is at most 4 in practice. `union` returns False when the two were already connected. That one bit is the answer to half the problems below. ## When does a problem need Union-Find? The wording again. - "How many connected components" or "how many provinces". - "Are these two nodes connected" asked many times. - "Which edge closes a cycle" in an undirected graph. - "Merge accounts", "group the emails", "friend circles". - Edges arrive one at a time, with a question after each. The common thread: groups that only merge, never split, and many membership queries. Signals, near misses and six code templates are on the [Union-Find pattern page](/patterns/union-find). ## How is Union-Find different from BFS or DFS? Both answer "are these connected". They differ in when the graph is known. | | Union-Find | BFS / DFS | | --- | --- | --- | | Graph is | Built edge by edge | Known in full up front | | Best at | Many connectivity queries | One traversal, paths, distances | | Finds the path? | No, only yes or no | Yes | | Cost per query | Near O(1) | O(V + E) | Need the actual path or the shortest one? That is [BFS or DFS](/patterns/graph-bfs-dfs). Need to count groups or catch the edge that closes a cycle? Union-Find. ## Which problems use Union-Find? Five that come up in interviews more than the rest. - Number of Connected Components. Start with n groups, subtract one per successful union. - Redundant Connection. The first union that returns False names the edge. - Number of Provinces. The same count on an adjacency matrix. - Accounts Merge. Union every email in one account, then group by root. - Kruskal's minimum spanning tree. Sort edges, union, skip the ones that return False. ## How do you solve Redundant Connection with Union-Find? The problem. A tree with n nodes got one extra edge. Find an edge whose removal gives a tree again. Return the last one in the input if there are several. A tree has no cycles. The extra edge is the one that closes a cycle. So walk the edges in order and union each pair. The first union that returns False is the answer. ``` def redundant_connection(edges): uf = UnionFind(len(edges) + 1) for a, b in edges: if not uf.union(a, b): return [a, b] ``` Trace it on edges (1, 2), (1, 3), (2, 3). The first two unions succeed. The third finds 2 and 3 already share root 1. Answer: [2, 3]. Four lines, and the whole difficulty was in the reading. That is typical for this pattern. ## How do you explain Union-Find in an interview? Three sentences before the code. - "The edges only ever join groups. I need fast membership and fast merge, so Union-Find." - "Each node points at a parent. Same root means same group." - "Path compression and union by size keep every operation near constant." Then the class. Then the trace above. Name the two tricks again at the end; they are what separates a pass from a partial. ## What goes wrong with Union-Find? Four mistakes cover almost every failed submission. - Comparing parents instead of roots. Two nodes can share a group and have different parents. - Skipping path compression. Trees turn into chains and `find` becomes O(n). - Using it on a directed graph. Union-Find has no direction; "A reaches B" is not "B reaches A". - Rebuilding the structure per query. The point is to keep one and reuse it. ## How do you make Union-Find stick? Write the class from memory, both tricks included. Then solve Number of Connected Components and Redundant Connection back to back. Recognising the pattern from the wording is the harder half. That is what [LeetCode patterns](/blog/leetcode-patterns) is about. The [placement test](/placement) measures it. ## FAQ ### What is the time complexity of Union-Find? Near O(1) per operation with path compression and union by size. Formally O(α(n)), where α is below 5 in practice. ### Does Union-Find work on a directed graph? No. It only tracks undirected connectivity. For direction you need a traversal. ### Can Union-Find split a group? No. Groups only merge. A problem that removes edges is usually solved backwards, adding them instead. ### Union by size or union by rank? Either. Both keep trees shallow. Size is easier to explain and also answers "how big is this group". --- title: "Kahn's algorithm in Python: topological sort, step by step" url: https://algopath.pro/blog/kahns-algorithm-python language: en summary: "Kahn's algorithm sorts a directed graph by counting incoming edges. The code in Python, a trace you can follow by hand, the wording that tells you a problem wants it, and the three ways it breaks." updated: 2026-10-15 --- # Kahn's algorithm in Python: topological sort, step by step **Short answer.** Kahn's algorithm is a topological sort that counts incoming edges. Take every node with no incoming edges. Remove it and lower the counts of its neighbours. Repeat until nothing is left to remove. If some nodes never came out, the graph has a cycle. ## What does a topological sort do? It lines up the nodes of a directed graph. Every edge then points left to right. The everyday version is a course list with prerequisites. A topological order is an order you can take the courses in. It only exists for a graph without cycles. If A needs B and B needs A, there is no order. ## How does Kahn's algorithm work? Four steps, none of them hard. - Count the incoming edges of every node. That is its in-degree. - Put every node with in-degree 0 in a queue. - Pop a node and append it to the answer. Lower the in-degree of each neighbour by one. - A neighbour that reaches zero goes into the queue. Trace it on a graph with edges A→C, B→C, C→D. | Step | Queue | Popped | Answer | | --- | --- | --- | --- | | 0 | A, B | | | | 1 | B | A | A | | 2 | C | B | A, B | | 3 | D | C | A, B, C | | 4 | | D | A, B, C, D | C only entered the queue at step 2. Two edges led into it, and both had to go first. ## How do you write Kahn's algorithm in Python? The graph comes as an edge list. Nodes are numbered 0 to n-1. ``` from collections import deque def kahn(n, edges): graph = [[] for _ in range(n)] indegree = [0] * n for a, b in edges: graph[a].append(b) indegree[b] += 1 queue = deque(v for v in range(n) if indegree[v] == 0) order = [] while queue: v = queue.popleft() order.append(v) for w in graph[v]: indegree[w] -= 1 if indegree[w] == 0: queue.append(w) if len(order) != n: return None # the graph has a cycle return order ``` The cycle check is built in. A node on a cycle never reaches zero. So it never reaches the answer. Time is O(V + E). Every node is popped once and every edge is lowered once. ## When does a problem want Kahn's algorithm? The wording gives it away before you start thinking. - "Courses and prerequisites", "can you finish all courses". - "Build order", "package dependencies", "task scheduling". - "Return any valid order in which…". - "Detect a cycle in a directed graph". - "Recover the alphabet from a sorted list of words". The common thread is directed dependencies. The question is an order, or whether one exists. The signals, the near misses and six code templates are on the [topological sort pattern page](/patterns/topological-sort). ## How is Kahn's algorithm different from the DFS version? There are two ways to topologically sort. Both give a valid order. - Kahn starts from nodes with no incoming edges. The order is built front to back. - DFS goes deep and records a node on the way out. The result is reversed at the end. - Kahn finds a cycle by counting. DFS finds one by meeting a grey node. - Kahn gives you layers for free: nodes that could run in parallel. Interviewers usually expect Kahn. It is easier to explain out loud. That is half the score. ## How do you solve Course Schedule with Kahn's algorithm? The problem. There are n courses. Some pairs say "take b before a". Can you finish all of them? Read it as a graph. Each pair is an edge b→a. The question is whether the graph has a cycle. Run `kahn`. If it returns an order, the answer is yes. If it returns None, no. Course Schedule II asks for the order itself. Same code, return the list instead of a boolean. Trace a small case out loud: n = 3, pairs (1, 0) and (2, 1). In-degrees are 0, 1, 1. The queue starts with 0. Popping 0 frees 1, popping 1 frees 2. Order: 0, 1, 2. Now add the pair (0, 2). Every node has in-degree 1. The queue starts empty, the order is empty, the answer is no. ## How do you explain Kahn's algorithm in an interview? Three sentences, in this order, before any code. - "The prerequisites form a directed graph. I need a topological order." - "I count incoming edges and start from the nodes with none. Removing a node frees its neighbours." - "Every node and edge is touched once, so O(V + E). If the order is short, there is a cycle." Then the code. Then the trace above on three nodes. Say the complexity once more at the end. ## Where does Kahn's algorithm break? Three mistakes account for most wrong answers. - An edge read backwards. The order comes out mirrored and every test fails. - The `len(order) != n` check left out. A cyclic graph returns a short list and no error. - A list with `pop(0)` instead of a deque. That is O(n) per step. ## How do you make Kahn's algorithm stick? Write `kahn` from memory. Then solve Course Schedule and Course Schedule II. They are the same problem; the second asks for the order. A day later, write it again without looking. Why the gap matters is in the piece on [spaced repetition](/blog/spaced-repetition). Recognising the pattern from the wording is a separate skill. It is covered in [LeetCode patterns](/blog/leetcode-patterns). ## FAQ ### What is the time complexity of Kahn's algorithm? O(V + E) time and O(V + E) space for the adjacency list. ### What does Kahn's algorithm return on a graph with a cycle? A partial order. That is why the answer's length is compared with the node count. ### Is the topological order unique? No. Whenever two nodes sit in the queue, either can go first. Both orders are valid. ### Why a queue rather than a stack? A stack works too. The queue gives a layer-by-layer order. That is the one problems usually ask for. --- title: "Two pointers vs sliding window, and three more pairs people confuse" url: https://algopath.pro/blog/two-pointers-vs-sliding-window language: en summary: "Two pointers and a sliding window both walk an array with two indices. What those indices mean separates them, and three more pairs work the same way." updated: 2026-10-15 --- # Two pointers vs sliding window, and three more pairs people confuse **Short answer.** Two pointers walk in from both ends of a sorted array and answer about a pair. A sliding window moves both indexes forward and answers about the contiguous range between them. To stop mixing the two up, log the pattern you reached for after every wrong answer. After twenty problems the log shows your pairs. Machine learning people call this a [confusion matrix](https://en.wikipedia.org/wiki/Confusion_matrix). Rows are the truth. Columns are the prediction. The interesting numbers sit off the diagonal. ## Why "wrong" is not enough A problem site gives you one bit. Solved, or not. A wrong answer breaks down into four separate ways to miss. Wrong pattern. Right pattern, forgotten step. Right approach, broken edge case. Plain typo. Each needs a different response. Collapsed into one red cross, you cannot pick. So you open another problem. This collapse is how people grind for months and stay put. ## Four pairs that catch nearly everyone **Two pointers and [sliding window](/patterns/sliding-window-variable).** Both walk an array with two indices. The window keeps a valid range and answers about that range. Two pointers converge from the ends and usually need the array sorted. Ask what the two indices mean, and the pair separates. **Prefix sums and a sliding window.** Windows want a sum that grows when you extend. One negative number kills that. [Prefix sums](https://en.wikipedia.org/wiki/Prefix_sum) take over. The signal is the phrase "may contain negative numbers". It is easy to read past. [Reading the statement twice](/blog/how-to-approach-a-coding-problem) catches it. **Backtracking and dynamic programming.** Both explore choices. [Backtracking](https://en.wikipedia.org/wiki/Backtracking) wants every arrangement. Its cost is the number of answers. [Dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming) wants one number. So it throws away everything except the best. Asking whether to list them or count them resolves most cases. **BFS and Dijkstra.** Identical until edges have weights. Unweighted, [breadth-first search](https://en.wikipedia.org/wiki/Breadth-first_search) is already shortest. Weighted, it silently returns the wrong answer. Silently wrong is worse than crashing, and it is why [Dijkstra](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) exists. ## What to log Three columns is enough. - What it actually was. - What you reached for. - The word in the problem that should have stopped you. The third column is the fix. A confusion is nearly always a signal you did not weigh. Which is the same claim as [pattern recognition being the trained skill](/blog/leetcode-patterns). By the fourth entry, the same word shows up again. ## What to do with a pair Do not reread both patterns. Write the one sentence that separates them, in your own words. "A window is valid, a two-pointer walk is sorted." "Backtracking lists, DP counts." Take two problems, one of each, back to back. For the first pair, the free lessons [two pointers from both ends](/steps/two-pointers-opposite-ends) and [a window that changes size](/steps/sliding-window-variable) make that drill. The contrast makes the sentence stick. Studying them a week apart is how the confusion formed. Gaps are for one pattern at a time: [spaced repetition](/blog/spaced-repetition). ## The honest part Your matrix will be personal. It will look nothing like a list of hard topics. Mine had heaps and sorting side by side for months. No curriculum pairs those two. Noticing your own pattern gaps is the point. You are collecting facts about yourself. There are fewer of them than it feels like. ## Where this is built in [AlgoPath](/) keeps this matrix for you. The drill records the pattern you named against the right one. The pairs you confuse come back more often. The first 33 steps are free. --- title: "How to talk during a coding interview" url: https://algopath.pro/blog/how-to-talk-during-a-coding-interview language: en summary: "The interviewer scores what you say, not what you think. Four things to say out loud, and how to rehearse them." updated: 2026-10-15 --- # How to talk during a coding interview **Short answer.** Say your decisions out loud, because the interviewer can only score what they hear. Four sentences cover it. Restate the input and output. Name the word that suggests a pattern. State the naive complexity before improving it. Test the empty case unprompted. Narrating your typing adds nothing. When stuck, say what you have ruled out. ## What they are actually filling in Most rubrics come down to four questions. Did you understand the problem. Did you choose an approach for a reason. Can you write correct code. Did you check your own work. Three of those four live in your head. Unsaid means scored as absent. An interviewer who cannot follow your reasoning assumes there was none. ## The four sentences Say these, roughly in this order, every time. **"So the input is X and I return Y."** Confirms the problem. Cheap insurance against twenty wasted minutes. **"The word that stands out is Z. That suggests a window."** This is the sentence people skip. That sentence is graded hardest. It shows recognition, not memory. **"The naive version is O(n squared). Let me say why before improving it."** Naming the slow version first is a strength. It shows you know what you are trading away. If the notation is rusty, start with [Big O notation explained simply](/blog/big-o-notation-explained-simply). **"Let me try the empty array."** Checking your own work unprompted moves you a whole band. ## The failure mode nobody warns you about People narrate their typing. "Now a variable called left, set to zero. Now a while loop." Narrating your typing is a screen reader. It carries no information the interviewer does not already have. Narrate decisions instead. Why two pointers and not a hash map. Why left moves here and not there. What has to stay true on every iteration. A decision has an alternative behind it. No alternative means it was not worth saying. ## What to do when you are stuck Say that you are stuck, then say what you have. "I know it is not a plain window because of the negatives. I am trying to decide between sorting and a prefix sum. Sorting loses the order, and I think the order matters here." Three sentences, two jobs. You stay scored while you think. The interviewer gets something to hint at. Silence gives them nothing. So they wait. Then the clock does the rest. ## How to practise it Talking while solving is its own skill. At first it steals attention from the solving. Researchers have used [think-aloud protocols](https://en.wikipedia.org/wiki/Think_aloud_protocol) for decades for that reason. Rehearse thinking out loud separately. - Solve one problem a week out loud, recorded, alone. Put it in the [timed slot](/blog/coding-interview-anxiety). - Listen back at double speed. Count how many sentences were decisions. - Explain a solved problem to someone who does not code. This is [rubber duck debugging](https://en.wikipedia.org/wiki/Rubber_duck_debugging) pointed at your own understanding. The recording is unpleasant. It is also the fastest feedback available. ## Where this is built in [AlgoPath](/) asks for the justification in writing first. Name the pattern. Say which signal pointed there. Then code. Write it every time. Saying it out loud becomes a short step. Where the spoken attempt sits: [how to prepare](/blog/how-to-prepare-for-coding-interviews). What to say first comes out of [reading the statement](/blog/how-to-approach-a-coding-problem). The first 33 steps are free. --- title: "Coding interview anxiety: practising under a timer" url: https://algopath.pro/blog/coding-interview-anxiety language: en summary: "A timer is a diagnostic, not a difficulty setting. When to add one, and the script for the moment anxiety takes over." updated: 2026-10-15 --- # Coding interview anxiety: practising under a timer **Short answer.** Practise without a timer until you can solve the pattern with the tab closed, then add one. A timer added earlier trains panic and nothing else. Anxiety takes up working memory, so move the state onto paper. Say the input and the output, solve a four-element example by hand, and watch what your hand did. ## What a timer actually removes Pressure does not make you slower at typing. It takes away working memory. [Working memory](https://en.wikipedia.org/wiki/Working_memory) holds the invariant, the loop bounds and the edge cases while you write. Anxiety occupies part of it. Ashcraft and Kirk showed this for maths in 2001. Same mechanism when you forget an approach you knew ten seconds ago. A timer takes the thing you were relying on. Holding several things in your head at once. Everything you had written down survives. That is the lever. ## Three phases, in order **Phase 1. No timer.** Solve until you can produce the pattern correctly with the tab closed. A timer here just tells you what you already know. **Phase 2. A generous timer.** 40 minutes for a problem you would normally finish in 20. It creates awareness of the clock without a threat. **Phase 3. The real thing.** 25 to 30 minutes, cold problem, out loud, no pausing. What to say out loud is in [how to talk during a coding interview](/blog/how-to-talk-during-a-coding-interview). Some pressure helps and too much stops helping. That curve has a name, the [Yerkes-Dodson law](https://en.wikipedia.org/wiki/Yerkes%E2%80%93Dodson_law). Most people jump straight to phase 3 in week one. Then they conclude they are bad at algorithms. The honest reading: they were tested before they were taught. ## The five-minute rescue Your mind goes blank. Here is the script. It needs no working memory at all. - Say the input and the output out loud. - Write a tiny example, four elements, by hand. - Solve that example on paper the slow way. Any way at all. - Look at what your hand did on each step. That is usually the algorithm. - Say which pattern it resembles. Say which word points there. That is [reading the statement](/blog/how-to-approach-a-coding-problem) under pressure. Every line is external. Paper, voice, a written example. Nothing asks you to hold state. Practise the script itself, not only the problems. You will need it once. It decides between a bad twenty minutes and a bad hour. ## Slow is a separate diagnosis Missing the time limit has three causes. Each needs a different fix. Too long deciding the pattern means [recognition work](/blog/leetcode-patterns). Too long writing means you have not written that structure enough times. Too long debugging means you skipped the edge cases at the start. Practise in the language you will use: [all six are supported](/blog/three-new-languages-typescript-java-csharp). Write down which one it was. Otherwise "I ran out of time" becomes the whole diagnosis. There is nothing to act on in that sentence. ## The one honest use of a timer A timer answers one question. Can you do this without the pause where you look something up? The timer's answer is worth knowing two weeks before an interview. It is worth very little two months before. ## Where this is built in [AlgoPath](/) keeps the timed work separate. The steps are untimed. The recognition drill and the gauntlet run on a clock. Both come after the pattern is in place. The first 33 steps are free. --- title: "Spaced repetition: why review on day 4, not tomorrow" url: https://algopath.pro/blog/spaced-repetition language: en summary: "A review that comes too early costs time and buys nothing. Where the gap should sit, and what spaced repetition does to memory." updated: 2026-10-15 --- # Spaced repetition: why review on day 4, not tomorrow **Short answer.** Review a new pattern on day 4, then day 11, then day 25. A review the next day comes too early: the memory is still there, so nothing gets rebuilt. The best gap is 10 to 20 percent of how long you need to remember. For an interview in 3 months, that is 9 to 18 days. ## The curve underneath this Ebbinghaus measured his own memory in 1885. He drew the [forgetting curve](https://en.wikipedia.org/wiki/Forgetting_curve). Retention falls fast at first, then flattens. Every successful review resets the fall and makes the next one shallower. The best review timing is later than feels safe. ## Where the numbers come from Cepeda and colleagues ran the largest version of this test in 2008. Cepeda and colleagues varied the gap between study and review. Then they tested people much later. The whole body of work goes under the [spacing effect](https://en.wikipedia.org/wiki/Spacing_effect). The best gap scaled with how long the memory had to last. Roughly 10 to 20 percent of the target interval. Interviewing in 3 months needs 9 to 18 days between reviews. Interviewing in 3 weeks needs 2 to 4 days between reviews. Reviewing binary search every evening for a month wastes the month. ## The ladder 1, 3, 7, 16, 35 days. Recall a pattern correctly and it moves up a rung. Fail it and it drops to day 1. The [Leitner system](https://en.wikipedia.org/wiki/Leitner_system) moves patterns up or down by rung. One property matters. Your weak patterns come back constantly. Your strong ones get out of the way. The first rung is short on purpose. A pattern learned an hour ago is fragile. One recall the next day makes the longer gaps survivable. ## The mistake that hides all of this Most preparation is organised by topic. A week on trees. A week on graphs. A week on dynamic programming. Every review inside that week happens while the material is still warm. So every review is easy, and easy means cheap. At the end you have finished eight topics. You can reliably do the last one. Mixing patterns in one session fixes it. Mixing patterns feels much worse. Now you have to work out which pattern this is before you start. That step is the one an interview tests. ## What to do this week Keep one list of patterns with a date next to each. Each day, take the ones that are due. For each, close everything and rebuild the core from memory. Correct, push the date out. Wrong, back to tomorrow. Ten minutes a day holds a lot more than a three-hour Sunday. What you do inside those ten minutes is [active recall](/blog/active-recall), not rereading. Where the schedule sits in a week: [how to prepare](/blog/how-to-prepare-for-coding-interviews). ## Where this runs [AlgoPath](/) schedules this for you. Recognition attempts feed a Leitner scheduler. The patterns you keep missing keep arriving. The solid ones back off. You do not keep the list. The track does. The first 33 steps are free. --- title: "Active recall: why it beats rereading" url: https://algopath.pro/blog/active-recall language: en summary: "Rereading a solution is the weakest way to learn an algorithm. Active recall rebuilds it from an empty page, and that is the strongest." updated: 2026-10-15 --- # Active recall: why it beats rereading **Short answer.** Active recall means closing the solution and rebuilding the algorithm from an empty page. Rereading feels smooth, and your brain takes that smoothness for knowledge. An interview asks for recall. In a 2006 study by Roediger and Karpicke, the recall group beat the rereaders a week later. Read a solution once, close it, rebuild it, compare. ## Why rereading feels right Read an explanation of Dijkstra twice. The second pass is smooth. Nothing surprises you. Your brain reads that smoothness as knowledge. What you built is recognition. The interview asks for recall. Recognition skills come apart at the worst moment. An empty editor. Someone watching. ## What the studies found The effect has a name: the [testing effect](https://en.wikipedia.org/wiki/Testing_effect). There is a large literature behind it. Roediger and Karpicke ran the standard version in 2006. Students read a passage. Then they either reread it or took a recall test. Tested five minutes later, the rereaders won. Tested a week later, the recall group won by a wide margin. Karpicke and Blunt repeated it in 2011 against concept mapping. That is a much more active kind of studying. Retrieval practice still won. The direction is consistent. Producing an answer beats reviewing one. ## What recall looks like for an algorithm Close every tab. Then, in order: - Say what the algorithm does in three sentences, out loud. - Write the core loop from memory. No notes, no autocomplete. - Name one production system that runs it. - Name the pattern it gets confused with, and the difference. Open your notes and mark what you missed. The marking is the whole exercise. Failing, then seeing the answer, beats reading that answer twice. ## It will feel worse Recall is uncomfortable. You sit in front of a blank page and nothing arrives. Robert Bjork calls this a [desirable difficulty](https://en.wikipedia.org/wiki/Desirable_difficulty). The effort of retrieval is what makes the memory durable. The session that felt bad is usually the one that worked. The smooth session taught you least. That inversion is why most people never switch. The feedback points the wrong way. ## One rule Never read a solution twice. Read it once. Close it. Rebuild it. Compare. Recall also pairs with the [spacing effect](https://en.wikipedia.org/wiki/Spacing_effect): the gap between attempts matters as much as the attempt. Which gap, in days, is in [spaced repetition](/blog/spaced-repetition). Tracking the spacing effect takes counting two numbers. How many problems you read last month. How many you could write today with the tab closed. For most people the second number is a shock. It is also the only one an interview measures. ## Where this is built in [AlgoPath](/) makes recall the default. Every step ends with a gate. Explain it with no notes. Rewrite the core from scratch. Say why it fits the real production case. You cannot finish a step by reading it. Proven steps are what the [levels](/blog/levels-and-a-public-leaderboard) are built from. Where recall sits in a week of preparation is in [how to prepare properly](/blog/how-to-prepare-for-coding-interviews). You recall the pattern first, and [that is trainable](/blog/leetcode-patterns). The first 33 steps are free. --- title: "How to approach a coding problem, word by word" url: https://algopath.pro/blog/how-to-approach-a-coding-problem language: en summary: "Read the problem twice before you write anything. One statement taken apart word by word, and what each phrase rules out." updated: 2026-10-15 --- # How to approach a coding problem, word by word **Short answer.** Read the problem twice before writing code. The first pass finds what is being asked. The second finds the constraints: subarray or subsequence, at most or exactly, negatives allowed or not, and the size of n. Those few words decide the algorithm. Write them down in six short lines, then start coding. Here is one statement, taken apart. ## The statement > Given an array of integers and a number k, return the length of the longest subarray whose sum is at most k. The array can contain negative numbers. 1 ≤ n ≤ 200000. Twenty-eight words. Six of them decide the algorithm. ## "subarray" A subarray is contiguous. A [subsequence](https://en.wikipedia.org/wiki/Subsequence) is not. One word, and half the solution space is gone. Contiguous points at a [window](/patterns/sliding-window-variable) or a [prefix sum](/patterns/prefix-sums). Non-contiguous points at sorting or dynamic programming. People misread this constantly. It is the single most expensive word on the page. ## "longest" You return a length. So you never need to store the subarray itself. You also need a maximum. One running best value. One comparison. A running best value and one comparison are the whole bookkeeping. ## "sum is at most k" At most, not exactly. Exactly k would push you towards a hash map of prefix sums. At most k is a range. Ordering helps. A hash map does not. Exactly k is where [prefix sums](https://en.wikipedia.org/wiki/Prefix_sum) in a hash map earn their keep. Two words apart in English. Two different data structures. ## "can contain negative numbers" Negative numbers in the input are the trap. With non-negative numbers only, growing a window grows the sum. That is what makes two pointers legal. Negatives break it. The sum stops moving in one direction. Shrinking a window can raise the sum. Two pointers are now wrong. Prefix sums with a sorted structure are right. One clause moved the answer from 5 lines to 20. The clause was in the statement the whole time. ## "1 ≤ n ≤ 200000" 200 thousand rules out anything quadratic. That is 40 billion operations. The reference for what each bound allows is [time complexity](https://en.wikipedia.org/wiki/Time_complexity). n log n fits comfortably within the bound. Sorting is affordable. So is a binary search inside a loop. The bound is the interviewer telling you which complexity they expect. Read it as an instruction. What each bound allows is in [time complexity from the constraints](/blog/time-complexity-from-constraints). ## Six lines before you code - What comes in, and what goes out. - The size of the input. - Contiguous or not. - Sorted or not. - The edges: empty, one element, all negative. - The pattern you think it is. And the word that made you think so. The last line carries the weight. The words that point at each pattern are listed in the [algorithm patterns cheat sheet](/patterns/cheat-sheet). Guessing happens when you cannot name the word. A lucky guess teaches nothing. ## Two minutes, not thirty seconds In a 45-minute interview, two minutes of reading feels expensive. If the clock empties your head, use the [five-minute rescue script](/blog/coding-interview-anxiety). The reading time is the cheapest two minutes you have. A misread problem costs the whole session. You find out at minute 30. The wrong code is already on the board. Reading fast is not a skill anyone is testing. The rest of the routine around this is in [how to prepare properly](/blog/how-to-prepare-for-coding-interviews). ## Where this is drilled On [AlgoPath](/) every step opens with signals. Those are the words in a problem that point at the pattern. [Naming the pattern](/blog/leetcode-patterns) comes before any code. The recognition drill hands you an unlabelled problem. You name the pattern and justify it first. The first 33 steps are free. --- title: "Levels, and a leaderboard anyone can see" url: https://algopath.pro/blog/levels-and-a-public-leaderboard language: en summary: "AlgoPath has counted your points for a while. Prove a step, earn points, climb the board." updated: 2026-10-15 --- # Levels, and a leaderboard anyone can see AlgoPath has counted your points for a while. Prove a step, earn points, climb the board. Points have a problem. They score you. They never say what to do next. "Earn 40 more points" is not a task. Nobody can act on it. AlgoPath now has levels. A level is built out of topics. ## Eight rungs AlgoPath has eight levels: Newcomer, Apprentice, Practitioner, Engineer, Specialist, Expert, Master, Architect. You reach a rung by closing whole topics. Not by collecting points. Apprentice wants Foundations. Practitioner wants Big-O, arrays and hash tables. Each rung lists everything it needs. A topic closes when every step in it is proven. Your own code. Its own tests. Half a topic earns nothing. That is deliberate. Cherry-picking the easy steps across five topics leaves five holes. Closing one topic leaves none. A rung, once reached, stays yours. Requirements may get stricter later. Nobody drops back down. Every rung has its own page. It names the topics it wants. It says how many steps that is. [The full ladder of eight rungs](/levels) is open to anyone. ## The number that helps Your dashboard now says what is left. "Next level: Engineer. 13 steps to go. Two pointers and windows: 1 of 5." A concrete next step is a task. You can open the site tomorrow and do part of it. The old line said "#7 · 42 pts". It scored you. It never pointed anywhere. ## The board is open now The leaderboard used to sit behind the login. Only the people already competing could watch the competition. That is a strange way to run a contest. The leaderboard is public now. Anyone can look. Names, avatars, points, steps proven, level. Search engines are told to stay away. A visitor can read it. Google does not index it. A checkbox on your profile lets you opt out of appearing. It removes you from the table, the rankings, and your player card. One switch, one effect. Your own progress is untouched either way. ## Mail when you climb Reach a rung and one short email arrives. It names the topic you closed. It names the rung ahead. It says how far away that rung is. One email per level. Not one per step. You can turn it off on your profile. The checkbox sits next to the newsletter one. ## About the free steps The first 33 steps are free. The topics after those are paid. Engineer needs 33 steps. That is the free part exactly, to the step. So free access takes you to Engineer and stops there. The ladder shows where the free part ends. That beats a paywall appearing with no warning. Why the track is ordered this way: [why I built AlgoPath](/blog/why-i-built-algopath). ## What did not change Points still rank the board. Streaks still count days. Nothing about proving a step has moved. Levels sit on top of work you already did. Every step you have ever proven was counted. Open your dashboard and your rung is already there. A week of practice around the rungs: [how to prepare](/blog/how-to-prepare-for-coding-interviews). Go and see which one you are on. --- title: "LeetCode patterns: the skill nobody trains" url: https://algopath.pro/blog/leetcode-patterns language: en summary: "You can write binary search. Telling that a problem wants one is a separate skill, and almost nobody trains it." updated: 2026-10-15 --- # LeetCode patterns: the skill nobody trains **Short answer.** LeetCode patterns are about 50 recurring problem shapes, each with a few signal words. Sorted input and a pair point to two pointers. A contiguous range plus longest or shortest points to a sliding window. Most developers can write binary search and still miss it in an unlabelled problem. Every tutorial announces the answer in its title. That order is [why people hate algorithms](/blog/i-hate-algorithms). ## What recognition is An experienced person reads a statement. Something fires in about 10 seconds. What fires is a shortlist. "This smells like a window." "This is a graph in disguise." "The answer is monotonic, so I can search over it." This recognition is a learned mapping. It runs from surface features to a small set of candidates. Chess players have the same thing for board positions. Psychologists call it [chunking](https://en.wikipedia.org/wiki/Chunking_(psychology)). Chunking comes from exposure. Exposure can be arranged. Most people get it 30 seconds at a time, as a prelude to an hour of coding. Practise it on its own and the ratio changes. ## The signals are few, and learnable Most interview problems come from about 50 patterns. Each has a handful of tells. **Two pointers from both ends.** The input is sorted. The answer is a pair. **Sliding window.** Contiguous, substring, subarray. Plus longest, shortest, or a count under a limit. **[Binary search](https://en.wikipedia.org/wiki/Binary_search) on the answer.** The smallest or largest value that still works. Checking one value is easy. **[Heap](https://en.wikipedia.org/wiki/Heap_(data_structure)).** The k best of something. Or the smallest item, over and over. **[Monotonic stack](/patterns/monotonic-stack).** For each element, the next thing bigger or smaller. **[Union-find](https://en.wikipedia.org/wiki/Disjoint-set_data_structure).** Things merge into groups. You keep being asked if two are in the same one. Six patterns. One line each. You can hold that today. The signal words of all 52 sit in one table, the [algorithm patterns cheat sheet](/patterns/cheat-sheet). Firing on them automatically is the work. ## Learn the near misses Signals are half of it. The other half is the pattern standing next to it, looking almost right. **Two pointers against sliding window.** Both walk two indices. Two pointers converge from the ends and need sorted input. A window moves both indices the same way. How to log the pairs you mix up is in [two pointers vs sliding window](/blog/two-pointers-vs-sliding-window). **Greedy against dynamic programming.** Both build an answer step by step. Greedy is safe only when a local choice can never be regretted. **BFS against DFS.** Both visit every node. Shortest path in an unweighted graph has one correct answer. **Sorting against a heap.** Both give you order. Top 3 of a million? Sorting the million is the wrong tool. Recognition breaks here. On the two that look alike. ## How to train it **Hide the category.** Shuffle a list, cover the tags, or use a source built for it. **Answer before solving.** The pattern, and the words that told you. 3 minutes. **Stop after 3 minutes.** Move on once you have the right answer. Solving eats the time you need for repetitions. **Log every miss as a pair.** What it was. What you said. **Mix old material in.** Recognition tested on this week's topic only reminds you which week it is. Twenty prompts an hour is normal. Two hundred is a few weeks of light practice. Recognition shifts at roughly two hundred prompts. Where this sits in a week of work is in [how to prepare properly](/blog/how-to-prepare-for-coding-interviews). ## What it feels like when it works You read a problem and your first thought is a category. You still do all the work of solving it. But you know what kind of work it is. The panic that comes from a blank statement stops arriving. Interviews change in a second way. A plausible approach in the first minute buys you a conversation. Four minutes of silence buys you a hint, and hints cost. ## Where I put this into practice The recognition trainer is why [AlgoPath](/) exists. 52 patterns with their signals and their near misses. 194 unlabelled prompts. A timed gauntlet. A matrix showing which pairs you personally swap. The first 33 steps are free, and the trainer works inside them. --- title: "How to prepare for coding interviews properly" url: https://algopath.pro/blog/how-to-prepare-for-coding-interviews language: en summary: "Fewer problems, each one used four times, with recognition trained on its own. What to do in the weeks before a coding interview." updated: 2026-10-15 --- # How to prepare for coding interviews properly **Short answer.** Train the four skills an interview checks one at a time: recognising the problem type, recalling the technique, handling edge cases and writing correct code. Most practice trains only the code. Add 3-minute recognition drills, review each problem on days 1, 4, 11 and 27, and do one timed attempt out loud each week. ## Split the four skills An interview answer needs four things. **Recognition.** Working out what kind of problem this is. **Recall.** Remembering how that kind is solved. **Boundaries.** Empty input, one element, no answer. **Coding.** Writing it without bugs. Most practice trains coding. It touches recall. It leaves the other two to chance. Interviews fail on the other two. Train them separately. That is the whole method. ## Practise recognition without solving One habit here brings the highest value. It costs 3 minutes a problem. Read the statement. Write two lines. What kind of problem this is. Which words told you. Check the answer here. If it's right, move on and leave it unsolved. If wrong, write down what you thought it was. Then solve it. You can do 20 of these in an hour. Solving 20 takes a day. You need unlabelled problems for this. Most sites sort by topic, so the answer sits above the question. Signals, pattern by pattern, are in [pattern recognition](/blog/leetcode-patterns). ## Use each problem four times Solve a problem once and the work evaporates. The fix is the [spacing effect](https://en.wikipedia.org/wiki/Spacing_effect). It is the best supported result in the learning literature. **Day 1.** Solve it. Badly is fine. **Day 4.** Read the statement. Name the pattern out loud. No code. **Day 11.** Write the core from a blank file. No notes. **Day 27.** Name the pattern in under 30 seconds. Failing a step drops you back one interval. Those gaps look odd on purpose. They sit just past the point where it starts to fade. Dragging it back is where the strength comes from. ## Recall, never re-read Re-reading your old solution feels productive. Familiar text produces a strong sense of knowing. That feeling dies in front of a blank editor. Producing the answer instead has its own name, the [testing effect](https://en.wikipedia.org/wiki/Testing_effect). Why it beats a second read is in [active recall](/blog/active-recall). Every review has to produce something. A blank file. Or say it out loud with the screen closed. Or write the approach on paper. My rule: produced nothing, reviewed nothing. ## Add the timer late An interview is a timed performance in front of a stranger. Practise calm and untimed for months, and your first timed attempt is the interview itself. When to add the clock is in [practising under a timer](/blog/coding-interview-anxiety). Wait until a topic is comfortable. Take an unseen problem. Give yourself 25 minutes. Talk the whole time, to an empty room. It feels ridiculous. Talking out loud is also what gets scored. Interviewers grade the thinking they can hear. The four sentences to say are in [how to talk during a coding interview](/blog/how-to-talk-during-a-coding-interview). Once a week is enough. ## Keep a confusion matrix Wrong pattern calls get written down: what it was, what you said. After 30 or 40, a shape appears. Almost everyone has three or four pairs they keep swapping. Two pointers against sliding window. [Greedy](https://en.wikipedia.org/wiki/Greedy_algorithm) against [dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming). BFS against DFS. Those pairs are your study plan. New problems mostly test what you already get right. ## A week that works 3 sessions of 45 minutes. One long session does less. Each session: 10 minutes of recognition, 25 minutes on one new problem, 10 minutes of review. One timed spoken attempt a week. Sunday: 10 minutes with the matrix. Pick next week's focus. Under 3 hours a week. Four months of that beats a month of 6 hour days. ## What to skip Top 150 lists, until you have the patterns. A list is an order of problems, and an order is not a curriculum. Memorising solutions. It works for one problem and fails for its neighbour. Interviewers change one detail on purpose. Videos of someone solving a problem they already know, unless you tried it first. Why the material feels hostile in the first place is in [why people hate algorithms](/blog/i-hate-algorithms). ## Where I put this into practice [AlgoPath](/) runs on this method. 194 unlabelled prompts. A review queue on the ladder above. A confusion matrix built from your own misses. A timed gauntlet. The first 33 steps are free, and the trainer works inside them. --- title: "3 new languages: Welcome TypeScript, Java and C#!" url: https://algopath.pro/blog/three-new-languages-typescript-java-csharp language: en summary: "TypeScript, Java and C# now run in the browser alongside JavaScript, Python and PHP. All 143 exercises have a starter in each of the six, and nothing installs." updated: 2026-09-09 --- # 3 new languages: Welcome TypeScript, Java and C#! AlgoPath added three languages: TypeScript, Java, and C#. - TypeScript - Java - C# With JavaScript, Python and PHP that makes six. All 143 exercises ship a starter in each one. Everything runs in your browser. Nothing installs, and nothing you write leaves the tab. ## Does the language change the exercise? No. The tests are the same in all six. The signature changes. The cases do not. Solve a step in Python today. Rewrite it in Java next month. Same bar both times. ## Are the hints in every language? Yes. Every hint that shows code now has a block per language. That is 1,676 new blocks. Each answer is dropped into its own step. Then it runs against that step's tests. A hint that does not solve the exercise cannot ship. The words changed too. 394 rungs used to explain the plan in JavaScript. Even to someone writing Python. ## Where is this in the track? In all 150 steps. Step 19 is a good first one. Two pointers, six cases. Three of them expect null. Why the track exists is in [why I built AlgoPath](/blog/why-i-built-algopath). Why it feels hostile early is in [why people hate algorithms](/blog/i-hate-algorithms). The first 33 steps are free in full. Pick your language above the editor and write it. --- title: "I hate algorithms, and here is what is actually going wrong" url: https://algopath.pro/blog/i-hate-algorithms language: en summary: "People who say they hate algorithms are usually hitting one of four walls. Each wall has a name and a fix." updated: 2026-10-15 --- # I hate algorithms, and here is what is actually going wrong **Short answer.** Most people hate algorithms for four reasons, and all four can be fixed. Tutorials show the answer before the problem. Nobody says which production system runs the code. The difficulty jumps from loops to interview problems with steps missing. A solved or failed mark hides which skill broke. Each fix takes minutes a day. ## 1. You see the answer before the question Open any [binary search](https://en.wikipedia.org/wiki/Binary_search) tutorial. A tutorial starts with the name. Then it shows the code. The interesting work already happened. Someone read a problem and decided it was binary search. The video starts after that. A real problem sounds different: "Find the smallest capacity that ships all packages in 10 days." Nothing in that sentence looks like a sorted array. **The fix.** Before you read a solution, write one line. What in the problem should have pointed you there? If you cannot write it, you have read the problem and nothing more. ## 2. Nobody says what the code is for "Reverse a [linked list](https://en.wikipedia.org/wiki/Linked_list)" is a riddle. I did it several times before I found out what it runs. An [LRU cache](https://en.wikipedia.org/wiki/Cache_replacement_policies) moves a used entry to the front. Undo walks back through a chain. A scheduler pulls a task out of a queue. Pulling a task off a queue is a normal Tuesday at work. **The fix.** For each algorithm, find one real system that runs it. [git bisect](https://git-scm.com/docs/git-bisect) is binary search. [Rate limiters](https://en.wikipedia.org/wiki/Rate_limiting) are sliding windows. Database indexes are balanced trees. Five minutes each. It turns trivia into machinery. ## 3. The difficulty jumps The usual path: variables, loops, functions, interview problems. A lot hides in that gap. Why an array lookup is instant. What a hash table does. Why instant stops being true at 10 million items. Nobody says it out loud. You fail a problem a stranger labelled Easy. You draw a conclusion about yourself. The conclusion is wrong. You were handed step 40 and told it was step 5. **The fix.** When something feels impossible, look for the missing step. It is usually one idea, and smaller than it looks. ## 4. The scoring tells you nothing A problem site gives you one bit. Solved, or not. Four separate things can fail. Recognising the pattern. Recalling the technique. Getting the edges right. Writing correct code. A red cross hides which one it was. So you fix nothing and open another problem. Skipping the fix is how people grind for months and stay still. **The fix.** After every problem, write down which of the four failed. Two weeks of that beats 100 more problems. Which kind of practice trains which skill is in [the best way to practice DSA](/best-way-to-practice-dsa). The week that carries all four is [how to prepare](/blog/how-to-prepare-for-coding-interviews). ## The part that really is hard Some algorithms are genuinely difficult. Dynamic programming takes time however it is taught. Most of the misery comes from the order it arrives in. Hide the reasoning. Strip the purpose. Skip the foundation. Then measure with one bit. Fix those four and the wall goes away. It still takes months. The work stops being unpleasant. That decides whether you finish. The longer version of why I care is in [why I built AlgoPath](/blog/why-i-built-algopath). ## Frequently asked questions ### Is it normal to hate data structures and algorithms? Yes. Most of the dislike traces back to the four problems above, and each has a fix that takes minutes a day. Dynamic programming stays hard for nearly everyone. ### Why are data structures and algorithms so hard? Mostly because of missing steps. Courses jump from loops to interview problems and skip ideas like why an array lookup is instant. The five pieces people miss most often are in [why algorithm problems are hard](/blog/why-are-algorithms-so-hard). ### Why do people hate LeetCode? It scores each problem with one bit, solved or not, so it never says which skill failed. Its Easy label also hides missing steps, and a failed Easy feels like a verdict on you. Writing down which of the four skills broke after each problem fixes most of that. ## Where I put this into practice [AlgoPath](/) is built on those four fixes. Signals come before the solution. Every step ends in a real production case. The ladder starts at ["what is an algorithm"](/steps/what-is-an-algorithm). The trainer shows which patterns you mix up, and each step's tests show which edge case broke. The first 33 steps are free. --- title: "Why I built AlgoPath" url: https://algopath.pro/blog/why-i-built-algopath language: en summary: "I solved problems for months and still froze in interviews. Here is what was missing, and what I built instead." updated: 2026-09-09 --- # Why I built AlgoPath I could write code. I had shipped things real people used. I still sat in front of an interview problem. No idea what to do. AlgoPath exists for that gap. ## What was actually wrong? I did what everyone recommends. Pick a problem, get stuck, read the solution, feel fine. Then get stuck again. Around problem 60 I noticed something. I was not getting better at solving. I was getting better at reading solutions. Only one of those shows up in an interview. Why that habit forms is in [why people hate algorithms](/blog/i-hate-algorithms). ## When did it become obvious? A problem asked for the longest stretch under some limit. I stared at it for 20 minutes. The answer was a [sliding window](/patterns/sliding-window-variable). I had solved four of those that same week. I knew how to write one. Spotting one was the part I could not do. No tutorial had ever covered that step. They all started after it. ## What is the step nobody teaches? Solving an algorithm problem has three parts. Work out what kind of problem it is. Recall how that kind is solved. Write the code. Courses teach parts two and three. Problem sites give you three. Part one decides interviews, and it is left to accident. Part one arrives after a few hundred problems. Most people quit first. Training that step on purpose is [pattern recognition](/blog/leetcode-patterns). ## What did I build? 150 steps in order. Each takes one idea, explains it, and makes you write it. Then it lands in a real system. A rate limiter, an LRU cache, an autocomplete. Alongside them: 52 patterns, each with the signals that point at it. And 194 problems with the label removed. You name the pattern before writing a line. The unlabeled problems build a map of what you confuse with what. ## How does one step work? Learn the signals before the code, on purpose. Write the core yourself in the browser, in JavaScript, Python or PHP. Then wire it into the real case. A step is done when you can explain it with no notes. And rewrite the core from a blank file. That is the [testing effect](https://en.wikipedia.org/wiki/Testing_effect) doing the checking. The step comes back days later, then weeks, then a month. Plain [spaced repetition](https://en.wikipedia.org/wiki/Spaced_repetition). ## Why is every lesson written three times? You pick the depth, step by step. Simple assumes nothing. Standard is how a working developer would say it. Academic gives invariants and precise complexity. Write only for beginners and everyone else leaves on page one. ## Who is it for? People who can write a loop and a function. And stall the moment a problem stops announcing what it wants. Career switchers and self-taught developers. People with a few years of work and no formal study. I am in that last group. This track is not for competitive programming. It will not get you through an interview next week. ## Why am I the one writing it? I am Elman Huseynov. Ten years as a software engineer. Zalando in Berlin now, Shopify and Scoutbee before that. More at [helloelman.com](https://helloelman.com). None of that stopped me freezing. That is the part worth saying out loud. I never studied computer science. Everything I know came from shipping, which leaves holes. This was one of them. I built the course while learning it. Every place the explanation slows down is a place I got stuck. ## Where do you start? The first 33 steps are free in full. Big O, hash tables, two pointers, sliding windows, binary search. Start at step 1 if those words are new. Take the [placement test](/placement) if you already write code. It takes seven minutes and no account. The whole map is on the [roadmap](/roadmap). AlgoPath is at [algopath.pro](/). ## Site pages --- title: "AlgoExpert vs AlgoPath: is AlgoExpert worth it" url: https://algopath.pro/algopath-vs-algoexpert language: en updated: 2026-10-22 --- # AlgoExpert vs AlgoPath: is AlgoExpert worth it Short answer. AlgoExpert is a curated list of 200 interview questions, each with a video walkthrough. AlgoPath makes you implement the algorithm before you see any solution. Want one tidy list with videos? AlgoExpert does that well. Can you follow any walkthrough and still not write the code alone? Start here. *Prices checked on the vendor's own page on 26 September 2026.* ## Side by side | | AlgoExpert | AlgoPath | | --- | --- | --- | | The unit of work | One interview question | One algorithm | | What you produce | A solution to that question | The algorithm, then a system that uses it | | Explanations | A two-part video per question | Written theory, then a hint ladder that nudges instead of answering | | Languages | Written solutions in 9 languages | Six, running in the browser: JS, TS, Python, PHP, Java, C# | | Timed practice | 4 timed assessments | A timed round on an unfamiliar problem | | Price | $119 for a year of All Access, 7 products | First 33 steps free, then one payment | ## Where AlgoExpert is better The videos. Every question has a walkthrough in two parts: the idea first, then the code. The bundle. The same $119 opens six more products, systems design among them. AlgoPath has no systems design at all. The list is short on purpose. 200 questions is a number you can finish. ## Is AlgoExpert worth it It is if you learn by watching someone reason and you want one list to work through. The price covers every product they sell, which is a fair deal if you use two of them. It is a weaker buy if your problem is recall. The video shows the answer before you have produced one, and a question you watched feels solved when it is not. The page shows $693 struck through beside a countdown. Judge the $119 on its own. ## Where AlgoPath is better Every step hands you a file with a signature, a TODO and the tests. You write the body. The tests stay red until it works. Then the same function goes into a small production case: a rate limiter, an LRU cache, a git bisect. You see the algorithm doing a job. A trainer drills the part interviews test first. You read an unlabelled problem and name one of the [52 patterns](/patterns) before you write a line. ## Using both Build the algorithm here first. Then take an AlgoExpert question that uses it and try it cold. Watch the video only after your own attempt. ## Verdict - One list, videos, and systems design in the same purchase: AlgoExpert. - You can follow any walkthrough and cannot write the code alone: AlgoPath. - You need hundreds of reps and company tags: [LeetCode Premium](/is-leetcode-premium-worth-it). - A pattern flowchart and tested code templates: [AlgoMonster](/algopath-vs-algomonster). ## Try it The [placement test](/placement) is 15 questions and needs no account. The first 33 steps are free, [prefix sums](/steps/prefix-sums) and [counting the subarrays that sum to k](/steps/subarray-sum-k) among them. [Full access](/pricing) is one payment, with no renewal. Weighing more options? Here are the [LeetCode alternatives compared](/leetcode-alternatives). --- title: "AlgoMonster vs AlgoPath: is AlgoMonster worth it" url: https://algopath.pro/algopath-vs-algomonster language: en updated: 2026-10-22 --- # AlgoMonster vs AlgoPath: is AlgoMonster worth it Short answer. AlgoMonster and AlgoPath both teach patterns. AlgoMonster gives you a flowchart from problem to algorithm and a code template for each. AlgoPath makes you write each algorithm from a TODO and name the pattern without a chart. Want a reference to look things up in? AlgoMonster. Want to recall both from memory? Start here. *Checked on the vendor's own page on 26 September 2026. AlgoMonster shows its price only after you sign in, so no price is quoted here.* ## Side by side | | AlgoMonster | AlgoPath | | --- | --- | --- | | How a pattern is chosen | A flowchart of yes or no questions about the problem | A trainer: name the pattern for an unlabelled problem, then justify it | | How the code is written | From a template per pattern | From a TODO, with tests that stay red until it works | | Extras | Speedrun mode, an algorithm visualizer, more courses in the same membership | A real production case per step, a timed round | | Free part | You can start free | The first 33 steps | | Price | Shown after sign-in | One payment, no renewal | ## Where AlgoMonster is better The flowchart. It is a good map of which question to ask about a problem, and it is public. The templates. Each pattern comes with code that is known to work, so a bug in an off-by-one is less likely. The membership covers more than algorithms. Low-level design is in the same subscription. ## Is AlgoMonster worth it It is if you want one place that tells you which pattern to reach for and hands you tested code for it. Their own FAQ says the flowchart is not for memorising. In an interview it is not on the screen, so the skill that counts is walking it in your head. A template has the same limit: it helps while it is open next to you. Sign in and read the price before you commit. It is not on the public page. ## Where AlgoPath is better Every step hands you a file with a signature, a TODO and the tests. You write the body. The tests stay red until it works. Then the same function goes into a small production case: a rate limiter, an LRU cache, a git bisect. You see the algorithm doing a job. A trainer drills the part interviews test first. You read an unlabelled problem and name one of the [52 patterns](/patterns) before you write a line. After ten of those you get a list of which patterns you mix up with which. ## Using both Read the AlgoMonster flowchart once to see the questions it asks. Then train here until you ask them without it. ## Verdict - A pattern reference and tested templates: AlgoMonster. - Naming the pattern and writing the code with nothing open: AlgoPath. - Videos for every problem: [NeetCode](/algopath-vs-neetcode). - Animations and a community: [Structy](/algopath-vs-structy). ## Try it The [placement test](/placement) is 15 questions and needs no account. The first 33 steps are free, [binary search for a boundary](/steps/binary-search-boundary) and [search in a rotated array](/steps/search-rotated) among them. [Full access](/pricing) is one payment, with no renewal. Weighing more options? Here are the [LeetCode alternatives compared](/leetcode-alternatives). --- title: "Structy vs AlgoPath: is Structy worth it" url: https://algopath.pro/algopath-vs-structy language: en updated: 2026-10-22 --- # Structy vs AlgoPath: is Structy worth it Short answer. Structy teaches each topic through a sequence of problems, with an animated explanation and a code walkthrough for every one. AlgoPath makes you build the algorithm itself, then use it in a working system. If you want to be walked through problem by problem, Structy is good at it. If walkthroughs no longer stick, start here. *Prices checked on the vendor's own page on 26 September 2026.* ## Side by side | | Structy | AlgoPath | | --- | --- | --- | | The unit of work | A problem inside a topic | One algorithm | | What you produce | A solution to that problem | The algorithm, then a system that uses it | | Explanations | An animated explanation and a code walkthrough per problem | Written theory, then a hint ladder that nudges instead of answering | | Community | A Discord server for members | None yet | | Free part | You can start the course free | The first 33 steps | | Price | $49 a month or $129 a year | One payment, no renewal | ## Where Structy is better The animations. A pointer moving across an array is easier to follow drawn than described. The walkthroughs are language specific. You see the solution written in the language you will interview in. A problem comes with its variants, so you drill one idea several times in a row. There is a community. AlgoPath has no Discord. ## Is Structy worth it Start with the free part. If the animations make ideas click for you, $129 for a year is a fair price for the rest. Pay monthly only if your interview is close. Three months at $49 already costs more than the year. It is a weaker buy if you can follow every walkthrough and still cannot write the code alone. A walkthrough is still someone else writing it. ## Where AlgoPath is better Every step hands you a file with a signature, a TODO and the tests. You write the body. The tests stay red until it works. Then the same function goes into a small production case: a rate limiter, an LRU cache, a git bisect. You see the algorithm doing a job. A trainer drills the part interviews test first. You read an unlabelled problem and name one of the [52 patterns](/patterns) before you write a line. Structy groups problems by topic, so the topic tells you the pattern before you have to find it. ## Using both Watch the Structy animation for a topic you find hard to picture. Then close it and build the algorithm here from the TODO. The animation gives you the picture, and the tests tell you whether you can produce the code. ## Verdict - You learn best from animations and want a community: Structy. - You follow walkthroughs fine and freeze on a blank file: AlgoPath. - You want one curated list with videos: [AlgoExpert](/algopath-vs-algoexpert). - A pattern flowchart with a code template for each pattern: [AlgoMonster](/algopath-vs-algomonster). ## Try it The [placement test](/placement) is 15 questions and needs no account. The first 33 steps are free, [counting with a hash map](/steps/hashing-map-set-frequency) and [the sliding window that changes size](/steps/sliding-window-variable) among them. [Full access](/pricing) is one payment, with no renewal. Weighing more options? Here are the [LeetCode alternatives compared](/leetcode-alternatives). --- title: "Is LeetCode Premium worth it" url: https://algopath.pro/is-leetcode-premium-worth-it language: en updated: 2026-10-22 --- # Is LeetCode Premium worth it Short answer. LeetCode Premium is worth it if you know which company you are interviewing with and your patterns are already solid. It adds company-tagged questions to the largest problem bank there is. It does not teach you to write an algorithm you have not written before. *Prices checked on the vendor's own page on 26 September 2026.* ## What it costs $35 a month, billed monthly, or $159 a year. The yearly plan works out to $13.25 a month. The page shows $39 and $299 struck through next to those. The prices to judge are $35 and $159. ## What Premium adds | Free tier | Premium | | --- | --- | | Most of the problem bank | Every problem, including the locked ones | | Discussion tab on each problem | Company-specific interview questions | | Weekly contests | A coding agent with 500 extra monthly credits and up to 30 solution analyses a day | ## Who should pay You have an interview at a named company within a month or two. The company tags are the one thing that is hard to get elsewhere. You already recognise the common patterns and need volume. The bank has more problems than anything else. Buy one month for a single interview. Buy the year only if you will use it for more than five months, since five months at $35 already passes $159. ## Who should not You open a problem and freeze. More problems will not fix that. The skill missing is recognition, and a problem bank labels nothing for you. You name the right approach and the code comes out broken. That is recall. Premium gives you more places to fail at it, and no path through it. How to train recall, recognition and speed one at a time is in [the best way to practice DSA](/best-way-to-practice-dsa). ## What AlgoPath does instead Every step hands you a file with a signature, a TODO and the tests. You write the body. The tests stay red until it works. Then the same function goes into a small production case: a rate limiter, an LRU cache, a git bisect. You see the algorithm doing a job. A trainer drills the part interviews test first. You read an unlabelled problem and name one of the [52 patterns](/patterns) before you write a line. Then go back to LeetCode, free or paid, and use it for what it is best at: volume. ## Verdict - Interview at a named company soon, patterns solid: one month of Premium. - Freeze on unfamiliar problems: train recognition first. - Can follow solutions and cannot write them: build the algorithms by hand. - A video for every problem on a list: [NeetCode](/algopath-vs-neetcode), free to watch. ## Try it The [placement test](/placement) is 15 questions and needs no account. The first 33 steps are free, [the fixed-size sliding window](/steps/sliding-window-fixed) and [difference arrays](/steps/difference-arrays) among them. [Full access](/pricing) is one payment, with no renewal. Weighing more options? Here are the [LeetCode alternatives compared](/leetcode-alternatives). --- title: "Impressum" url: https://algopath.pro/impressum language: en updated: 2026-07-31 --- # Impressum Information pursuant to § 5 DDG (German Digital Services Act). ## Responsible party algopath.pro - Elman Huseynov c/o Online-Impressum #9942 Europaring 90 53757 Sankt Augustin, Deutschland ## Contact Email: contact@elman.group Contact form: https://algopath.pro/en/contact ## VAT USt-IdNr.: DE459148722 ## Supervisory authority Medienanstalt Berlin-Brandenburg (mabb), Berlin, Deutschland ## Responsible for content (§ 18 (2) MStV) algopath.pro - Elman Huseynov c/o Online-Impressum #9942 Europaring 90 53757 Sankt Augustin, Deutschland ## Consumer dispute resolution We are neither obliged nor willing to take part in a dispute resolution procedure before a consumer arbitration board. ## Liability for content As a service provider we are responsible for our own content on these pages under general law. We are not obliged to monitor transmitted or stored third-party information, or to investigate circumstances that indicate illegal activity. Obligations to remove or block the use of information under general law remain unaffected. Liability in this respect is only possible from the point in time at which a concrete infringement of the law becomes known. If we become aware of such infringements, we will remove the content immediately. ## Liability for links Our site contains links to external websites over whose content we have no influence. We cannot accept any liability for this third-party content. The respective provider or operator of the linked pages is always responsible for their content. ## Copyright The content and works created by the site operator on these pages are subject to copyright. Contributions from third parties are marked as such. Reproduction, editing, distribution and any kind of use beyond the limits of copyright require the written consent of the respective author or creator. --- title: "Datenschutz" url: https://algopath.pro/datenschutz language: en updated: 2026-07-31 --- # Datenschutz This policy explains what personal data AlgoPath collects, why, and what rights you have. It follows the EU General Data Protection Regulation (GDPR). ## Controller algopath.pro - Elman Huseynov c/o Online-Impressum #9942 Europaring 90 53757 Sankt Augustin, Deutschland Email: contact@elman.group ## Hosting The site is hosted on shared web hosting by Hostinger International Ltd. The provider processes the server log data described below on our behalf under a data processing agreement. ## Account data You sign in either with Google or GitHub, or with an email address and a password. When you use Google or GitHub we receive your name and email address from that provider; we never receive your password there. When you use email and password, the password is stored only as a cryptographic hash. You can optionally add profile details such as a display name and links to your profiles on other services. These are voluntary and not needed to use the course. Legal basis: performance of a contract (Art. 6 (1)(b) GDPR); for the optional profile fields, your consent (Art. 6 (1)(a) GDPR). ## Learning progress We store which steps you have opened, passed and completed, your streak of active days, your review schedule, and your position on the leaderboard. This lets the course continue across visits. Legal basis: performance of a contract (Art. 6 (1)(b) GDPR). ## Payments Paid access is handled by Stripe, which sells it in its own name as merchant of record. You enter your payment details on Stripe only; we never see or store a card number. For a purchase we store the amount paid, the currency, the Stripe checkout identifier, the country of your billing address and the tax Stripe reported. Country and tax are needed for our own bookkeeping. Before payment we ask you to agree to performance starting immediately. For that consent we store the time and the IP address, because we must be able to prove that the right of withdrawal expired early. Legal basis: performance of a contract (Art. 6 (1)(b) GDPR) and legal obligation (Art. 6 (1)(c) GDPR) for record-keeping. Stripe privacy policy: https://stripe.com/privacy. ## Withdrawal from a contract If you use the "Vertrag widerrufen" button we store your name, email address, order reference, and the times the request was received and confirmed. We do not ask for a reason. Legal basis: legal obligation (Art. 6 (1)(c) GDPR). ## Forms and messages What you send through the contact form and the bug-report form is stored so the request can be handled. You may optionally attach a screenshot to a bug report; images are re-encoded on upload, which drops any embedded metadata. For the newsletter we store your email address until you unsubscribe. Every message carries an unsubscribe link. Legal basis: legitimate interest in answering enquiries (Art. 6 (1)(f) GDPR); for the newsletter, your consent (Art. 6 (1)(a) GDPR). ## Code you write Your solutions to the exercises run entirely in your browser through WebAssembly. The code is not sent to or executed on our servers. If you ask for it to be saved, the current state of your solution is stored so you can carry on later. ## Newsletter For the newsletter we store your email address, your name and the moment you agreed. We store the moment because we have to be able to prove the consent (§ 7 (2) no. 2 UWG). We also record which message went to you and when - without that we could send you the same message twice. Legal basis: your consent (Art. 6 (1)(a) GDPR). You can withdraw it at any time, through the unsubscribe link in every message or the checkbox in your profile. Withdrawing does not affect the lawfulness of what was processed before. Messages are sent from our own server at Hostinger; we use no external marketing service. When delivery measurement is switched on, a message carries a 1×1 pixel and links that route through our server, which shows us whether it was opened and whether a link was clicked. Those numbers are approximate and we treat them as such: anyone who blocks images never registers as an open, and some mail programs fetch images on their own without anybody reading the message. Blocking remote images in your mail program prevents open measurement entirely. When measurement is switched off, messages carry no pixel and no redirected links. ## Cookies and consent Necessary cookies keep the site working: a session cookie keeps you signed in, one cookie remembers your language, and a "cookie_consent" cookie stores your choice on the consent banner. These need no consent (Art. 6 (1)(f) GDPR). We use no advertising cookies. Analytics cookies are set only if you accept them in the banner - see "Web analytics" below. The contact and bug-report forms use Google reCAPTCHA to block spam. reCAPTCHA is a non-essential third party, so we load it only after you accept non-essential cookies in the banner. If you decline, reCAPTCHA is never loaded and the forms are protected by a hidden field and rate limiting instead. Legal basis for reCAPTCHA: your consent (Art. 6 (1)(a) GDPR). You can withdraw consent at any time by clearing the "cookie_consent" cookie in your browser; the banner then appears again. reCAPTCHA is provided by Google Ireland Ltd - see https://policies.google.com/privacy. ## Public leaderboard AlgoPath keeps a leaderboard. Anyone who has completed steps appears on it with the name and picture from their own account, the points they earned, how many steps they completed and the level they reached. The same details are shown on their player card. Both are readable without an account. Search engines are told not to index these pages. That does not stop a visitor from reading them. You can opt out at any time from your profile ("Hide me from the leaderboard"). After that you appear neither in the table nor in its rankings, and your player card is no longer reachable. Your own progress is unaffected. Legal basis: our legitimate interest in a visible comparison between learners (Art. 6 (1)(f) GDPR), together with the opt-out that is available at any time. ## Traffic sources We keep a count of where visitors arrive from, so we know which of our posts and links actually bring people here. This is our own measurement - no third party is involved and no additional cookie is set. Once per browsing session we store a single record: the source name (for example "reddit"), the kind of link (social network, search, referral, direct), a campaign name if the link carried one, the domain the visit came from (the domain only, never the full address), the page that was opened first, and the time. We do not store your IP address, and we do not store your browser's user agent. The record is tied only to your current session, which ends after two hours of inactivity; a later visit is counted as a new and unconnected one. If you create an account during that session, the record is linked to your account so we can see which channel the account came from. You can ask us to remove that link at any time, and it is deleted together with your account in any case. Legal basis: our legitimate interest in understanding which of our own publications work (Art. 6 (1)(f) GDPR). No consent is required for this, because nothing is stored on or read from your device beyond the session cookie that is strictly necessary for the site to work. Anonymous records are deleted after 13 months. ## Web analytics When web analytics is switched on, we load Google Analytics 4 (Google Ireland Ltd., Gordon House, Barrow Street, Dublin 4, Ireland) - and only after you have accepted non-essential cookies in the banner. If you decline, or if analytics is switched off, nothing is loaded and no analytics cookie is set. What is processed: the pages you open, the time and length of the visit, an approximate location derived from a shortened IP address, device type, browser, and the page you arrived from. Google Analytics 4 does not store the IP address. We use no Google Tag Manager container, so no further third-party scripts can be loaded through it. Legal basis: your consent (Art. 6 (1)(a) GDPR, § 25 (1) TTDSG). You can withdraw it at any time by clearing the "cookie_consent" cookie in your browser; the banner then appears again. Google privacy policy: https://policies.google.com/privacy. ## Error monitoring When something fails on the server, the application reports it to Sentry (Functional Software, Inc., through Sentry GmbH, Berlin) so we can fix it. What goes out is the technical error report, the route that was requested and the time. We use Sentry's European region, so the error reports are processed inside the EU. No personal data is sent with it: the send_default_pii option is switched off, so no IP address, no account details and no form contents are transmitted. We cannot rule out entirely that an individual error message contains personal data. Legal basis: legitimate interest in a working and secure service (Art. 6 (1)(f) GDPR). Sentry privacy policy: https://sentry.io/privacy. ## Server log files The hosting provider automatically records standard access data (IP address, date and time, page requested, browser and operating system). This data is used to keep the service secure and running and is not combined with other data about you. Legal basis: legitimate interest (Art. 6 (1)(f) GDPR). ## Third parties your data reaches Google (login, reCAPTCHA on forms and, with your consent, web analytics), GitHub (login), Stripe (payments) and Hostinger (hosting). Each processes data under its own privacy policy. We share only what each service needs for its function. ## Transfers to the United States Google, GitHub and Stripe belong to groups headquartered in the United States, so data can reach that country. The transfer rests on the European Commission's adequacy decision of 10 July 2023 on the EU-US Data Privacy Framework where the recipient is certified under it, and otherwise on the standard contractual clauses under Art. 46 (2)(c) GDPR. Even on those grounds, access by US authorities cannot be ruled out entirely. Fonts and every other design asset on this site are served from our own server, so simply opening a page sends nothing to a third party. ## How long we keep data Account and progress data are kept while your account exists. Purchase records and withdrawals are kept as long as tax and commercial law require. When you delete your account, we delete your personal data except records we must retain by law. ## Your rights You have the right to access, rectify, erase, restrict and port your data, and to object to processing. To exercise any of these, email us at contact@elman.group. You also have the right to complain to a data protection supervisory authority - in Berlin, the Berliner Beauftragte für Datenschutz und Informationsfreiheit. --- title: "Terms and conditions" url: https://algopath.pro/agb language: en updated: 2026-09-13 --- # Terms and conditions These terms govern the purchase and use of full access to the AlgoPath course. They apply to consumers and businesses alike. ## 1. Operator algopath.pro - Elman Huseynov c/o Online-Impressum #9942 Europaring 90 53757 Sankt Augustin, Deutschland Email: contact@elman.group USt-IdNr.: DE459148722 ## 2. What is sold AlgoPath is an online course on data structures and algorithms: written lessons, exercises with automated tests, and a code runner that executes your solutions in your own browser. A number of steps at the start of the course are free and need no purchase. Buying full access unlocks the exercises of every remaining step. Access runs for 12 months from the moment it is unlocked. It does not renew and nothing further is charged: there is no subscription, nothing to renew and nothing to cancel. After the 12 months the operator keeps access open for as long as the course is offered; there is no entitlement to that. ## 3. Price and seller The price shown on the pricing page is the final price. Tax is included; nothing is added at checkout. Payment is processed by Stripe as merchant of record. Stripe is your contractual partner for the payment itself and issues the receipt; it collects and remits any applicable sales tax or VAT. Access to the course is provided by the operator named above. Payment is due in full at checkout. Access is granted automatically once Stripe confirms the payment. ## 4. How the contract is concluded Presenting the price on this site is an invitation to buy, not a binding offer. You place a binding order by confirming the purchase on the checkout page and completing payment. The contract is concluded when access is granted to your account, which happens automatically after Stripe confirms payment. The contract text is not stored in a form you can retrieve later. Keep the confirmation email and the receipt Stripe sends you. ## 5. Right of withdrawal If you are a consumer you have a statutory right to withdraw from the contract within 14 days. The details, and the button to exercise it, are on the Widerrufsbelehrung page. This right ends early if you expressly ask for the content to be made available immediately and confirm that you lose the right of withdrawal by doing so. The checkout page asks for exactly that before payment, and records your answer. ## 6. Account and use Access is personal. Do not share your account or the course material with others, and do not republish the lessons or exercises. Your solutions are yours. Code you write runs in your own browser and is never executed on our servers. Access may be withdrawn without refund if the account is shared, the material is redistributed, or the platform is attacked or abused. ## 7. Availability The course is offered as it is at the time of purchase. Content is added and corrected over time; that is included in the access you bought, not billed separately. The operator may discontinue the course once your 12 months have passed. If the course is discontinued within your 12 months, the price is refunded in proportion to the time remaining. Continuous availability cannot be guaranteed. Short interruptions for maintenance, hosting failures, or causes outside the operator’s control do not entitle you to a refund. ## 8. Liability The operator is liable without limitation for intent, gross negligence, and for injury to life, body or health. For ordinary negligence, liability is limited to breaches of obligations essential to the purpose of the contract, and to the damage typically foreseeable in such a contract. The course is educational material. No promise is made about the outcome of any interview, exam or hiring process. ## 9. Consumer dispute resolution We are neither obliged nor willing to take part in dispute resolution proceedings before a consumer arbitration board. ## 10. Applicable law German law applies, excluding the UN Convention on Contracts for the International Sale of Goods. If you are a consumer resident in the EU, the mandatory consumer protection rules of your country of residence remain unaffected. If any provision of these terms is invalid, the remainder stays in force. --- title: "Widerrufsbelehrung" url: https://algopath.pro/widerrufsbelehrung language: en updated: 2026-07-31 --- # Widerrufsbelehrung Right of withdrawal for consumers. A consumer is any natural person who enters into a contract for purposes that are mainly outside their trade, business or profession. ## Right of withdrawal You have the right to withdraw from this contract within 14 days without giving any reason. The withdrawal period is 14 days from the day the contract was concluded. To exercise it, you must inform us of your decision by a clear statement. The quickest way is the withdrawal button below; an email works too. To meet the deadline it is enough to send the notice before the period expires. ## Where to send it algopath.pro - Elman Huseynov c/o Online-Impressum #9942 Europaring 90 53757 Sankt Augustin, Deutschland Email: contact@elman.group ## Consequences of withdrawal If you withdraw, we refund all payments received from you without undue delay and at the latest within 14 days of the day we receive your notice. The refund is made using the same means of payment you used, unless expressly agreed otherwise; you are not charged any fee for it. Payments run through Stripe as merchant of record, so the refund is issued by Stripe. ## Early expiry of the right The right of withdrawal expires early for digital content if you have expressly agreed that we begin performance before the withdrawal period ends, and you have confirmed that you thereby lose your right of withdrawal. The checkout page asks for both before payment and records your answer with a timestamp. Without that consent, the 14-day right applies in full. ## Model withdrawal form "To Elman Huseynov, contact@elman.group: I hereby withdraw from the contract concluded by me for the purchase of access to the AlgoPath course. Ordered on: … . Name of the consumer: … . Address of the consumer: … . Date: … ." Two steps, no sign-in needed. Once you confirm, you get an email with the exact date and time your withdrawal was submitted. [Vertrag widerrufen](/en/widerruf) --- title: "LeetCode alternatives: ten sites compared" url: https://algopath.pro/leetcode-alternatives language: en updated: 2026-10-22 --- # LeetCode alternatives: ten sites compared Short answer. Stay on LeetCode if you want volume and company-tagged questions. Use NeetCode if you want a curated list with videos. Want contests under a clock? Codeforces or AtCoder. Solved a hundred problems and still cannot write binary search from memory? Look at AlgoPath. *Updated 15 October 2026.* ## Sites like LeetCode, side by side | Platform | What you actually do | Best for | Price | | --- | --- | --- | --- | | [LeetCode](https://leetcode.com) | Solve problems from a bank of thousands | Volume, contests, company tags | Free tier, Premium $35 a month or $159 a year | | [NeetCode](https://neetcode.io) | Work a curated list with a video per problem | Being shown how a solution is found | Free videos and lists, Pro $119 a year or $297 once | | **AlgoPath** | Implement each algorithm yourself, then wire it into a working system | Writing the code from memory | First 33 steps free, then one payment | | [AlgoExpert](https://www.algoexpert.io) | Curated problems with video walkthroughs | A guided list | $119 for a year | | [Codewars](https://www.codewars.com) | Community kata, ranked by difficulty | Daily reps in your own language | Free | | [HackerRank](https://www.hackerrank.com) | Practice tracks plus the test many firms screen with | Getting used to the screening format | Free tier | | [Exercism](https://exercism.org) | Exercises reviewed by a human mentor | Learning a language, not interview prep | Free | ## Which of these are free Two of the seven in the table let you practise everything for free: Codewars and Exercism. Their paid extras (Codewars Red at $5 a month, Exercism Insiders through a donation) add stats and remove ads. No exercise sits behind them. LeetCode and HackerRank both have a free tier. On LeetCode the payment adds company-tagged questions and the problems marked Premium. On HackerRank it buys extra AI mock interviews. AlgoPath gives the first 33 steps free, with the runner and the tests. Full access is one payment rather than a subscription. ## Contest and puzzle sites Three more sites come up in every list of LeetCode alternatives. They are problem archives and contests, with no course on patterns. | Platform | What you actually do | Best for | Price | | --- | --- | --- | --- | | [Codeforces](https://codeforces.com) | Rated contests plus a large problem archive | Speed under a clock, competitive programming | Free | | [AtCoder](https://atcoder.jp) | Weekly contests, the beginner rounds start easy | A first taste of contests | Free | | [Project Euler](https://projecteuler.net) | Maths-heavy problems where you submit the number, not the code | Number theory and clever counting | Free | These build volume and speed. AlgoPath steps train you to spot the pattern in a problem and write the algorithm from memory, so the two go well together. ## Where LeetCode is still the better choice It has more problems than anything else. Each one has a discussion tab. Premium adds the company tags. Know which firm you are sitting with? That list is hard to replace. Contests run weekly. That is real practice under a clock. Interview in two weeks and patterns already solid? Then you need volume. Go there. ## What AlgoPath does differently - One step is one algorithm, and you implement it. 150 steps, in a fixed order. - Six languages run in the browser. JavaScript, TypeScript, Python, PHP, Java and C#. Nothing to install. - Every step ends in a system that really uses the algorithm. A rate limiter. An LRU cache. A git bisect. - A trainer for what interviews test first. Read a problem, name the [pattern](/patterns). - Everything is written in English and Russian. ## Which one you need depends on what breaks Run one mock interview and watch where it goes wrong. - You read the problem and freeze. That is recognition. Drill the patterns. - You name the right approach and the code comes out broken. That is recall. Build the algorithms by hand. - Both fine, just slow. That is volume. Grind problems. ## Head-to-head comparisons Each one says where the other product wins. Prices are read off the vendor's own page. - [AlgoPath vs NeetCode](/algopath-vs-neetcode) - [Is AlgoExpert worth it](/algopath-vs-algoexpert) - [Is Structy worth it](/algopath-vs-structy) - [Is AlgoMonster worth it](/algopath-vs-algomonster) - [Is LeetCode Premium worth it](/is-leetcode-premium-worth-it) ## Frequently asked questions ### Is LeetCode free? Mostly. Most of the problem bank and the weekly contests are open without paying. Premium, at $35 a month or $159 a year, adds company-tagged questions and the problems marked Premium. ### What is the best free alternative to LeetCode? It depends on what you practise. Codeforces has a large free archive and rated contests. Codewars suits short daily problems in your own language. Exercism suits learning a new language with a human reviewer. The first 33 AlgoPath steps are free too, runner and tests included. ### Is there a site like LeetCode but easier? Codewars starts at 8 kyu, its easiest rank, and the AtCoder Beginner Contests open with problems a newcomer can solve. AlgoPath starts one step earlier, at [what an algorithm is](/steps/what-is-an-algorithm), and adds one idea per step. ### Is LeetCode or HackerRank better? For practice, LeetCode: more problems, weekly contests and company tags. Many companies send their screening test through HackerRank, so a few of its practice tracks are worth doing before a real test, to learn its editor. ### Is LeetCode or Codeforces better for interviews? LeetCode. Codeforces problems are written for contests, with more maths and tighter time limits than most interviews ask for. Use Codeforces for speed and LeetCode for the interview format. ## Try it before you decide The [placement test](/placement) is 15 questions and needs no account. It says which topic to start on. The first 33 steps are free, runner and tests included. They go as far as [2D prefix sums](/steps/prefix-2d) and [binary search on the answer](/steps/binary-search-on-answer). [Full access](/pricing) is one payment, with no subscription. Not sure yet? Read [how to practice](/best-way-to-practice-dsa) before you pick anything. --- title: "Is NeetCode worth it? AlgoPath vs NeetCode, compared" url: https://algopath.pro/algopath-vs-neetcode language: en updated: 2026-10-16 --- # Is NeetCode worth it? AlgoPath vs NeetCode, compared Short answer. NeetCode shows you how a solution is found. AlgoPath makes you write the algorithm before you ever see one. If you learn well by watching, start with NeetCode. If watching has stopped moving you forward, come here. The first 33 AlgoPath steps are free, so trying it costs nothing. *Updated 15 October 2026.* ## Side by side | | NeetCode | AlgoPath | | --- | --- | --- | | The unit of work | One interview problem | One algorithm | | What you produce | A solution to that problem | The algorithm, then a system that uses it | | Explanations | A video per problem | Written theory, then a hint ladder that nudges instead of answering | | Languages | Solutions in several | Six, running in the browser: JS, TS, Python, PHP, Java, C# | | Where your code runs | Your own editor | In the browser, with the tests | | Order | A curated list, pick as you like | 150 steps in a fixed chain | | Free part | The videos and the NeetCode 150 and 250 lists | The first 33 steps | | Paid part | Pro: $119 a year or $297 once | One payment, no renewal | ## Where NeetCode is better The videos. Hundreds of them, free, and the explanations are clear. The NeetCode 150 is a good list. Someone hands you an order and talks you through every answer. [AlgoExpert](/algopath-vs-algoexpert) sells the same kind of list, 200 questions with a video each, for a yearly fee. AlgoPath has no video library of that size. ## Is NeetCode worth it The free half is worth it for almost anyone. Hundreds of clear video explanations cost nothing, and no subscription is needed to watch them. Pro is a separate question. It costs $119 a year or $297 once, and it adds video courses and a larger problem bank with its own code runner. The NeetCode 150 and 250 lists stay free without it. Pay for it if you want courses that teach each topic from the start. Skip it if your problem is that you can follow a solution and cannot produce one. That gap is the one thing a video cannot close. Watching someone reason is not the same as reasoning. ## Where AlgoPath is better Watching a solution and producing one are two different skills. This trains the second. Every step hands you a file with a signature and a TODO. Tests come with it. You write the body. The tests stay red until it works. Then the same function goes into a small production case. You see the algorithm doing a job. There is also a trainer for [pattern recognition](/patterns), 52 of them. And a timed round for speaking under pressure. ## Using both This is the common route, and it works. Build the algorithm here first. Then watch the NeetCode video on a problem that uses it. You get to see somebody else reason through it. Order matters. Watch first and you remember a solution. Build first and you keep the algorithm. ## Verdict - New to all of this and want a guided path: NeetCode. - Know the theory, need reps and company tags: [LeetCode Premium](/is-leetcode-premium-worth-it). - Can follow any solution but cannot produce one: AlgoPath. - You learn best from animations and want a community: [Structy](/algopath-vs-structy). ## Frequently asked questions ### Is NeetCode free? Mostly. The video solutions, the NeetCode 150 and 250 lists and the topic roadmap are free. Only Pro costs money. ### How much is NeetCode Pro? $119 for a year or $297 as a one-time payment, as listed on neetcode.io/pro in October 2026. Sales come and go, so check the page before you buy. ### Is the NeetCode 150 enough for interviews? For coverage, yes: 150 problems in 18 topics, from arrays and hashing to dynamic programming. It is enough once you can write each solution again in an empty file a week later. If you only recognise the answer after the video, keep going. ### Should I start with NeetCode or LeetCode? NeetCode, if you need an order. Its lists are LeetCode problems put in a sequence, with a video each. Move to LeetCode itself later for volume and company tags. ## Try it The [placement test](/placement) is 15 questions, no account needed. The first 33 steps are free, [two pointers](/steps/two-pointers-opposite-ends) and [a sliding window with a counter](/steps/sliding-window-hash-counter) among them. [Full access](/pricing) is one payment. Weighing more than these two? Here are the [alternatives compared](/leetcode-alternatives). --- title: "The best way to practice DSA" url: https://algopath.pro/best-way-to-practice-dsa language: en updated: 2026-10-16 --- # The best way to practice DSA Short answer. Train three things separately. Writing each algorithm from memory. Naming the pattern a new problem needs. Doing both against a clock, out loud, in 35 minutes. Most people only do the third and wonder why it stays hard. Grinding a problem bank trains speed, and recall needs an empty file. *Updated 15 October 2026.* ## The three skills an interview actually tests 1. **Recall.** Write binary search, a heap, a union-find, with nothing open. If you cannot, more problems will not fix it. Why rebuilding beats rereading is in [active recall](/blog/active-recall). 2. **Recognition.** Read 200 words and name the pattern inside a minute. This is where most candidates lose the round. 3. **Speed under pressure.** Both of the above, out loud, in 35 minutes. ## What each method trains | Method | Trains | Leaves untouched | | --- | --- | --- | | Grinding a problem bank | Speed, exposure | Recall. You read the editorial and move on. | | Watching solution videos | Recognition, a little | Recall. Watching is not writing. | | Implementing algorithms from an empty file | Recall | Speed, and choosing the right algorithm | | Flashcards on complexity | Facts | Everything your hands do | | Mock interviews | Speed, speaking | Nothing new. It measures, it does not teach. | ## A week that works - Monday to Wednesday: one algorithm a day. From an empty file, until its tests pass. - Thursday: five problems you have never seen. Name the pattern before writing a line. Do not code yet. - Friday: two problems in 35 minutes, spoken out loud. - Weekend: rewrite whichever algorithm you got wrong on Monday. Four hours a week beats one Sunday marathon. Recall fades on a schedule, so touch it often. ## The mistake that eats the most time Opening the editorial at minute 10. Give it 25 minutes. Then take a hint, not the answer. A hint you needed marks a hole you now know about. ## How to tell it is working - You name the pattern before you finish reading. - You write the skeleton without pausing to remember the loop bounds. - You can say why this algorithm and not the neighbouring one. ## Where this is built in AlgoPath runs in exactly this order. 150 steps, one algorithm each, written by you. Tests run in the browser, in six languages. The first 33 are free, starting with [what an algorithm is](/steps/what-is-an-algorithm). [Big-O](/steps/big-o-and-constraints) arrives at step 9. Then a trainer across 52 [patterns](/patterns) for recognition. Then a timed round for speed. Start with the [placement test](/placement). 15 questions, no account. It says which topic to open first. Weighing platforms? There is a [comparison of the alternatives](/leetcode-alternatives) too. ## Основные страницы - [AlgoPath: алгоритмы и структуры данных через собственную реализацию](https://algopath.pro/ru) - [Цены: один платёж за 12 месяцев доступа, без подписки](https://algopath.pro/ru/pricing) - [Карта курса: 150 шагов от массивов до графов](https://algopath.pro/ru/roadmap) - [Уровни: что значит каждый ранг](https://algopath.pro/ru/levels) - [Вводный тест: с какой темы начать](https://algopath.pro/ru/placement) - [Паттерны алгоритмов и какой из них нужен задаче](https://algopath.pro/ru/patterns) - [Шпаргалка по паттернам алгоритмов: все на одной странице](https://algopath.pro/ru/patterns/cheat-sheet) - [Калькулятор сложности алгоритма онлайн: оценка Big O](https://algopath.pro/ru/big-o-calculator) - [Глоссарий: 387 терминов по алгоритмам и структурам данных](https://algopath.pro/ru/glossary) - [Блог: подготовка к собеседованиям и паттерны алгоритмов](https://algopath.pro/ru/blog) - [Отзывы тех, кто занимался на AlgoPath](https://algopath.pro/ru/reviews) - [Контакты: задать вопрос о курсе](https://algopath.pro/ru/contact) ## Паттерны алгоритмов --- title: "Преобразование массива на месте" url: https://algopath.pro/ru/patterns/in-place-array language: ru summary: "Индекс записи идёт следом за индексом чтения по одному буферу. Назад копируется только нужное, второй массив не создаётся." updated: 2026-10-17 --- # Преобразование массива на месте Индекс записи идёт следом за индексом чтения по одному буферу. Назад копируется только нужное, второй массив не создаётся. ## Преобразование массива на месте: как это работает? Два индекса идут по одному массиву. read смотрит каждую ячейку, write помечает место для следующего нужного значения. read сдвигается на каждом шаге цикла. write сдвигается только тогда, когда значение прошло проверку. Нужное значение копируется в arr[write]. После этого write сдвигается на одну ячейку. Лишнее значение оставляет write на месте. Следующее нужное перезапишет эту ячейку. write никогда не обгоняет read, поэтому копия не портит непрочитанные данные. Одного буфера хватает именно поэтому. В конце write хранит новую логическую длину. Ячейки после неё это остатки. Источники: [Wikipedia (англ.): In-place algorithm](https://en.wikipedia.org/wiki/In-place_algorithm) - `[1, 1, 2, 2, 3] write=1 read=1` Убираем дубликаты в отсортированном массиве. Первое значение остаётся всегда. - `[1, 1, 2, 2, 3] write=1 read=2` arr[1] повторяет arr[0] и отбрасывается. write не двигается. - `[1, 2, 2, 2, 3] write=2 read=3` arr[2] новое, поэтому попадает в ячейку 1. write сдвигается. - `[1, 2, 2, 2, 3] write=2 read=4` arr[3] повторяет уже сохранённую двойку. Значение отбрасывается. - `[1, 2, 3, 2, 3] write=3` Тройка ложится в ячейку 2. Ответ это длина 3, хвост мусорный. ## Преобразование массива на месте: когда применять? Паттерн «Преобразование массива на месте» нужен, когда в условии есть слова «изменить массив на месте», «O(1) дополнительной памяти» или «не выделять ещё один массив/строку». Решение на нём обычно укладывается в O(n). - изменить массив на месте - O(1) дополнительной памяти - не выделять ещё один массив/строку - развернуть/повернуть/разбить/переместить элементы в одном буфере - вернуть новую длину после удаления дубликатов ## Преобразование массива на месте: с чем путают? Паттерн «Преобразование массива на месте» чаще всего путают с этими паттернами: Два указателя (в одну сторону), Хеш-множество / словарь, Элементарные сортировки (выбором, пузырьком, вставками) и Префиксные суммы. - **Два указателя (в одну сторону)** - Пара медленный и быстрый это один из способов переписать буфер. Работа на месте здесь цель, а не способ. - **Хеш-множество / словарь** - Множество убирает дубликаты за проход, но хранит все ключи. Работа на месте нужна как раз ради памяти. - **Элементарные сортировки (выбором, пузырьком, вставками)** - Сортировка вставками тоже правит один буфер, но меняет порядок. Здесь порядок выживших не меняется. - **Префиксные суммы** - Префиксные суммы строят второй массив накопленных сумм. Именно его тут и хотят избежать. ## Преобразование массива на месте: сложность по времени и памяти У паттерна «Преобразование массива на месте» целевая сложность O(n). n до 1e6 и один проход дают O(n) времени. Память остаётся O(1), потому что ничего не выделяется. ## Преобразование массива на месте: разбор примера ### Сдвинуть нули в конец Дан массив чисел, все нули надо перенести в конец. Порядок ненулевых значений должен сохраниться. Выделять второй массив нельзя. Идите по массиву индексом чтения. Копируйте каждое ненулевое значение в arr[write] и двигайте write. После прохода write указывает, где начинаются нули. Заполните нулями всё оттуда. ```javascript function moveZeroes(nums) { let write = 0; for (let read = 0; read < nums.length; read++) { if (nums[read] !== 0) { nums[write] = nums[read]; write++; } } // everything from write onwards is a leftover slot while (write < nums.length) { nums[write] = 0; write++; } return nums; } ``` ## Преобразование массива на месте: частые ошибки - **Двигают write на каждом шаге** Тогда write и read совпадают и ничего не удаляется. write двигается только при сохранении. - **Режут хвост прямо во время прохода** В остаточных ячейках лежат значения, до которых read ещё не дошёл. Обрезайте их после цикла. - **Возвращают массив вместо длины** Такие задачи обычно просят новую длину. Сам буфер сохраняет исходный размер. - **Сравнивают не с той ячейкой** Проверка дубликата смотрит на arr[write - 1], а не на arr[read - 1]. Ранние ячейки уже перезаписаны. ## Преобразование массива на месте: задачи с собеседований Паттерн «Преобразование массива на месте» решает такие задачи с собеседований: Удаление дубликатов из отсортированного массива, Удаление элемента, Сдвиг нулей, Сортировка цветов, Поворот массива, Разворот строки и Удаление дубликатов из отсортированного массива II. - **Удаление дубликатов из отсортированного массива** Оставляйте значение, только если оно отличается от arr[write - 1]. - **Удаление элемента** Оставляйте всё, что не равно заданному значению. - **Сдвиг нулей** Скопируйте ненулевые вперёд, потом добейте хвост нулями. - **Сортировка цветов** Три индекса переписывают один буфер за один проход. - **Поворот массива** Разверните весь массив, потом разверните каждую из двух частей. - **Разворот строки** Меняйте местами с обоих концов внутрь без нового буфера. - **Удаление дубликатов из отсортированного массива II** Оставляйте значение, пока оно отличается от arr[write - 2]. ## JavaScript ```javascript function inPlaceTransform(arr) { let write = 0; for (let read = 0; read < arr.length; read++) { if (shouldKeep(arr[read])) { arr[write] = arr[read]; write++; } } return write; // new logical length } ``` ## TypeScript ```typescript declare function shouldKeep(value: number): boolean; function inPlaceTransform(arr: number[]): number { let write = 0; for (let read = 0; read < arr.length; read++) { if (shouldKeep(arr[read])) { arr[write] = arr[read]; write++; } } return write; // new logical length } ``` ## Python ```python def in_place_transform(arr): write = 0 for read in range(len(arr)): if should_keep(arr[read]): arr[write] = arr[read] write += 1 return write # new logical length ``` ## PHP ```php function inPlaceTransform(array &$arr): int { $write = 0; for ($read = 0; $read < count($arr); $read++) { if (shouldKeep($arr[$read])) { $arr[$write] = $arr[$read]; $write++; } } return $write; // new logical length } ``` ## Java ```java public static int inPlaceTransform(int[] arr) { int write = 0; for (int read = 0; read < arr.length; read++) { if (shouldKeep(arr[read])) { arr[write] = arr[read]; write++; } } return write; // new logical length } private static boolean shouldKeep(int value) { throw new UnsupportedOperationException("your rule here"); } ``` ## C# ```csharp public static int InPlaceTransform(int[] arr) { int write = 0; for (int read = 0; read < arr.Length; read++) { if (ShouldKeep(arr[read])) { arr[write] = arr[read]; write++; } } return write; // new logical length } private static bool ShouldKeep(int value) { throw new NotImplementedException("your rule here"); } ``` --- title: "Хеш-множество / словарь" url: https://algopath.pro/ru/patterns/hash-set-map language: ru summary: "Хеш превращает поиск в один шаг. Множество отвечает, встречалось ли значение, словарь отвечает, сколько раз оно встречалось." updated: 2026-10-17 --- # Хеш-множество / словарь Хеш превращает поиск в один шаг. Множество отвечает, встречалось ли значение, словарь отвечает, сколько раз оно встречалось. ## Хеш-множество / словарь: как это работает? Хеш-функция превращает ключ в номер ячейки. Значение кладётся именно туда. Поиск запускает ту же функцию заново. Он сразу попадает в ячейку, без перебора. Два ключа могут дать одну ячейку. Структура хранит оба и сравнивает сами ключи. Множество хранит только ключи. Оно отвечает на вопрос, видел ли я это. Словарь хранит рядом с ключом значение. Он держит счётчики, индексы или последнюю позицию. Вставка, поиск и удаление стоят в среднем O(1). Плата за это O(n) памяти. Источники: [Википедия: Хеш-таблица](https://ru.wikipedia.org/wiki/%D0%A5%D0%B5%D1%88-%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D0%B0), [MDN (англ.): Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) - `seen = {}` Ищем первое повторяющееся значение в [3, 1, 3, 4]. - `seen = {3}` Тройка раньше не встречалась. Она попадает в множество. - `seen = {3, 1}` Единица тоже новая. В множестве уже два ключа. - `seen = {3, 1}` Тройка приходит второй раз. Множество её уже содержит. - `ответ = 3` Ответ возвращается сразу. Последний элемент так и не прочитан. ## Хеш-множество / словарь: когда применять? Паттерн «Хеш-множество / словарь» нужен, когда в условии есть слова «встречалось ли это раньше», «посчитать сколько каждого» или «найти дубликаты». Решение на нём обычно укладывается в O(n). - встречалось ли это раньше - посчитать сколько каждого - найти дубликаты - есть ли подходящее/дополняющее значение - данные в любом порядке / не отсортированы ## Хеш-множество / словарь: с чем путают? Паттерн «Хеш-множество / словарь» чаще всего путают с этими паттернами: Два указателя (с концов), Скользящее окно (переменное), Сортировка без сравнений (подсчётом / поразрядная), Trie (префиксное дерево) и Префиксные суммы. - **Два указателя (с концов)** - Двум указателям нужен отсортированный массив, зато лишней памяти нет. Хеш нужен, когда сортировать нельзя. - **Скользящее окно (переменное)** - Окно отвечает на вопрос про один непрерывный отрезок. У словаря понятия отрезка нет вообще. - **Сортировка без сравнений (подсчётом / поразрядная)** - Сортировка подсчётом тоже считает частоты, но ей нужны маленькие целые ключи. Словарь берёт любой ключ. - **Trie (префиксное дерево)** - Trie делит общие префиксы, поэтому отвечает на запросы по префиксу. Словарь сравнивает только целые ключи. - **Префиксные суммы** - Префиксные суммы отвечают про диапазоны в исходном порядке. Хеш порядок не хранит. ## Хеш-множество / словарь: сложность по времени и памяти У паттерна «Хеш-множество / словарь» целевая сложность O(n). n до 1e6 неотсортированных значений даёт O(n) времени. Память O(n), потому что хранится каждый ключ. ## Хеш-множество / словарь: разбор примера ### Сумма двух на неотсортированных данных Дан неотсортированный массив и число-цель. Верните индексы двух значений, дающих эту сумму. Сортировка сломала бы исходные индексы. Значит массив трогать нельзя. Пройдите массив один раз. Для каждого значения посчитайте, какого дополнения не хватает. Если дополнение уже в словаре, пара найдена. Иначе положите значение вместе с индексом. ```javascript function twoSum(nums, target) { const seen = new Map(); // value -> index for (let i = 0; i < nums.length; i++) { const need = target - nums[i]; if (seen.has(need)) { return [seen.get(need), i]; } // stored after the lookup, so a value never pairs with itself seen.set(nums[i], i); } return []; } ``` ## Хеш-множество / словарь: частые ошибки - **Кладут в словарь до проверки** Вставляйте значение только после поиска. Иначе значение составит пару само с собой. - **Берут обычный объект вместо Map** Объект молча делает из любого ключа строку, и 1 совпадает с "1". Используйте Map. - **Считают без значения по умолчанию** Чтение отсутствующего ключа даёт undefined, а undefined плюс один это NaN. Начинайте счёт с нуля. - **Платят памятью там, где не нужно** Если данные уже отсортированы, два указателя обойдутся без памяти. Хеш нужен, когда порядок бесполезен. ## Хеш-множество / словарь: задачи с собеседований Паттерн «Хеш-множество / словарь» решает такие задачи с собеседований: Сумма двух, Есть ли дубликаты, Проверка анаграммы, Группировка анаграмм, K самых частых элементов, Самая длинная последовательность подряд и Подмассив с суммой k. - **Сумма двух** Кладите каждое значение с индексом и ищите дополнение. - **Есть ли дубликаты** Множество, которое отвергает повтор, отвечает за один проход. - **Проверка анаграммы** Посчитайте буквы одного слова, потом вычтите буквы второго. - **Группировка анаграмм** Ключ это отсортированное слово. Значение это группа. - **K самых частых элементов** Посчитайте словарём, потом возьмите k наибольших счётчиков. - **Самая длинная последовательность подряд** Множество позволяет спросить, есть ли x минус один. - **Подмассив с суммой k** Храните, сколько раз встречалась каждая накопленная сумма. ## JavaScript ```javascript function firstDuplicate(arr) { const seen = new Set(); for (const x of arr) { if (seen.has(x)) return x; seen.add(x); } return null; } ``` ## TypeScript ```typescript function firstDuplicate(arr: number[]): number | null { const seen = new Set(); for (const x of arr) { if (seen.has(x)) return x; seen.add(x); } return null; } ``` ## Python ```python def first_duplicate(arr): seen = set() for x in arr: if x in seen: return x seen.add(x) return None ``` ## PHP ```php function firstDuplicate(array $arr) { $seen = []; foreach ($arr as $x) { if (isset($seen[$x])) return $x; $seen[$x] = true; } return null; } ``` ## Java ```java public static Integer firstDuplicate(int[] arr) { Set seen = new HashSet<>(); for (int x : arr) { if (seen.contains(x)) return x; seen.add(x); } return null; } ``` ## C# ```csharp public static int? FirstDuplicate(int[] arr) { var seen = new HashSet(); foreach (int x in arr) { if (seen.Contains(x)) return x; seen.Add(x); } return null; } ``` --- title: "Два указателя (с концов)" url: https://algopath.pro/ru/patterns/two-pointers-opposite language: ru summary: "Два индекса стартуют по краям отсортированного массива и идут навстречу. Каждый шаг двигает ту сторону, что мешает ответу." updated: 2026-10-17 --- # Два указателя (с концов) Два индекса стартуют по краям отсортированного массива и идут навстречу. Каждый шаг двигает ту сторону, что мешает ответу. ## Два указателя (с концов): как это работает? Один индекс стоит на первой ячейке, другой на последней. Вместе они задают кандидата на ответ. Прочитайте значения на обоих концах и сложите их. Получится сумма, ширина или пара для сравнения. Сравните результат с целью. Сравнение говорит, какой конец мешает. Если комбинация мала, сдвиньте левый индекс вправо. Порядок гарантирует, что значение только вырастет. Если она велика, сдвиньте правый индекс влево. Это единственный ход, который её уменьшит. Цикл кончается, когда индексы встретились. Каждый элемент прочитан максимум один раз. Источники: [Wikipedia (англ.): 3SUM](https://en.wikipedia.org/wiki/3SUM) - `[2, 4, 5, 8, 11] lo=0 hi=4` Цель равна 12. Указатели стартуют по краям. - `2 + 11 = 13` Сумма на единицу больше цели. Уменьшить её может только правый конец. - `[2, 4, 5, 8, 11] lo=0 hi=3` Теперь 2 плюс 8 это 10, и этого мало. Увеличить может только левый конец. - `[2, 4, 5, 8, 11] lo=1 hi=3` Теперь 4 плюс 8 это 12. Цель достигнута точно. - `ответ = [1, 3]` Три сравнения на пять элементов. Ничего не прочитано дважды. ## Два указателя (с концов): когда применять? Паттерн «Два указателя (с концов)» нужен, когда в условии есть слова «отсортированный массив (или можно отсортировать)», «найти пару/тройку с заданной суммой» или «сжать с обоих концов (контейнер, площадь)». Решение на нём обычно укладывается в O(n). - отсортированный массив (или можно отсортировать) - найти пару/тройку с заданной суммой - сжать с обоих концов (контейнер, площадь) - проверка палиндрома ## Два указателя (с концов): с чем путают? Паттерн «Два указателя (с концов)» чаще всего путают с этими паттернами: Скользящее окно (переменное), Хеш-множество / словарь, Два указателя (в одну сторону) и Бинарный поиск (массив). - **Скользящее окно (переменное)** - Окно хранит живой отрезок, и оба конца идут вперёд. Эти указатели стартуют врозь и сходятся. - **Хеш-множество / словарь** - Хеш находит дополнение в неотсортированных данных ценой O(n) памяти. Указателям нужен порядок, но не память. - **Два указателя (в одну сторону)** - Там оба индекса идут вперёд: один читает, другой пишет. Здесь они сближаются с разных концов. - **Бинарный поиск (массив)** - Бинарный поиск ищет одну фиксированную цель и делит отрезок пополам. Здесь сравниваемая пара меняется каждый шаг. ## Два указателя (с концов): сложность по времени и памяти У паттерна «Два указателя (с концов)» целевая сложность O(n). n до 1e6 на отсортированных данных даёт O(n). Каждый указатель делает не больше n шагов, память O(1). ## Два указателя (с концов): разбор примера ### Контейнер с наибольшей водой Каждое число это высота вертикальной линии на графике. Выберите две линии, между которыми поместится больше всего воды. Площадь равна меньшей из линий, умноженной на расстояние между ними. Начните с самой широкой пары, по линии на каждом краю. Сдвигать более высокую линию бессмысленно: площадь ограничена низкой. Значит двигайте низкую и запоминайте лучшую площадь. ```javascript function maxArea(height) { let lo = 0; let hi = height.length - 1; let best = 0; while (lo < hi) { const area = Math.min(height[lo], height[hi]) * (hi - lo); best = Math.max(best, area); // the shorter side caps the area, so it is the only one worth moving if (height[lo] < height[hi]) lo++; else hi--; } return best; } ``` ## Два указателя (с концов): частые ошибки - **Забывают, что вход обязан быть отсортирован** Весь приём держится на порядке. На неотсортированных данных сравнение указывает не туда. - **Пишут lo <= hi** При lo <= hi индекс составит пару сам с собой. Для пары разных элементов нужно lo < hi. - **Двигают оба конца за один шаг** Тогда верная пара может быть пропущена без проверки. Двигайте ровно один конец за шаг. - **Выдают одну тройку дважды** После совпадения пропускайте равные значения с обеих сторон. Иначе дубликаты вернутся как разные ответы. ## Два указателя (с концов): задачи с собеседований Паттерн «Два указателя (с концов)» решает такие задачи с собеседований: Сумма двух II (отсортированный вход), Сумма трёх, Контейнер с наибольшей водой, Проверка палиндрома, Сбор дождевой воды, Сортировка цветов и Квадраты отсортированного массива. - **Сумма двух II (отсортированный вход)** Базовая форма: двигайте тот конец, что чинит сумму. - **Сумма трёх** Зафиксируйте одно значение и пустите два указателя по остатку. - **Контейнер с наибольшей водой** Двигайте низкую сторону, она ограничивает площадь. - **Проверка палиндрома** Сравните концы и шагните внутрь. - **Сбор дождевой воды** Двигайте сторону с меньшей стенкой и копите воду. - **Сортировка цветов** Два конца собирают нули и двойки. - **Квадраты отсортированного массива** Наибольший квадрат всегда лежит на одном из концов. ## JavaScript ```javascript function twoPointers(arr, target) { let lo = 0; let hi = arr.length - 1; while (lo < hi) { const sum = arr[lo] + arr[hi]; if (sum === target) return [lo, hi]; if (sum < target) lo++; else hi--; } return null; } ``` ## TypeScript ```typescript function twoPointers(arr: number[], target: number): [number, number] | null { let lo = 0; let hi = arr.length - 1; while (lo < hi) { const sum = arr[lo] + arr[hi]; if (sum === target) return [lo, hi]; if (sum < target) lo++; else hi--; } return null; } ``` ## Python ```python def two_pointers(arr, target): lo, hi = 0, len(arr) - 1 while lo < hi: s = arr[lo] + arr[hi] if s == target: return (lo, hi) if s < target: lo += 1 else: hi -= 1 return None ``` ## PHP ```php function twoPointers(array $arr, int $target): ?array { $lo = 0; $hi = count($arr) - 1; while ($lo < $hi) { $sum = $arr[$lo] + $arr[$hi]; if ($sum === $target) return [$lo, $hi]; if ($sum < $target) $lo++; else $hi--; } return null; } ``` ## Java ```java public static int[] twoPointers(int[] arr, int target) { int lo = 0; int hi = arr.length - 1; while (lo < hi) { int sum = arr[lo] + arr[hi]; if (sum == target) return new int[] { lo, hi }; if (sum < target) lo++; else hi--; } return null; } ``` ## C# ```csharp public static int[] TwoPointers(int[] arr, int target) { int lo = 0; int hi = arr.Length - 1; while (lo < hi) { int sum = arr[lo] + arr[hi]; if (sum == target) return new int[] { lo, hi }; if (sum < target) lo++; else hi--; } return null; } ``` --- title: "Два указателя (в одну сторону)" url: https://algopath.pro/ru/patterns/two-pointers-same-direction language: ru summary: "Указатель чтения бежит вперёд, а указатель записи идёт следом. Запись сдвигается только там, где значение стоит сохранить." updated: 2026-10-17 --- # Два указателя (в одну сторону) Указатель чтения бежит вперёд, а указатель записи идёт следом. Запись сдвигается только там, где значение стоит сохранить. ## Два указателя (в одну сторону): как это работает? Оба индекса стартуют с начала массива. read читает, а write помечает следующее место записи. read сдвигается на каждом шаге цикла. Он смотрит каждый элемент ровно один раз. Проверка решает, оставить ли текущее значение. Только эта проверка и меняется от задачи к задаче. При сохранении значение копируется в ячейку write. После этого write сдвигается на один. При отбрасывании write стоит на месте. Ячейка ждёт следующее выжившее значение. write отстаёт от read ровно на число отброшенных. Поэтому начало массива до write и есть ответ. Источники: [Wikipedia (англ.): In-place algorithm](https://en.wikipedia.org/wiki/In-place_algorithm) - `[3, 2, 3, 4, 2] write=0` Убираем все тройки. arr[0] это тройка, поэтому запись не идёт. - `[2, 2, 3, 4, 2] write=1` arr[1] это двойка, она остаётся. Значение уходит в ячейку 0. - `[2, 2, 3, 4, 2] write=1` arr[2] это ещё одна тройка. write остаётся на месте. - `[2, 4, 3, 4, 2] write=2` arr[3] это четвёрка, она остаётся. Значение ложится в ячейку 1. - `[2, 4, 2, 4, 2] write=3` Последняя двойка ложится в ячейку 2. Ответ это длина 3. ## Два указателя (в одну сторону): когда применять? Паттерн «Два указателя (в одну сторону)» нужен, когда в условии есть слова «удалить дубликаты на месте, сохранив порядок», «переместить/разбить элементы с сохранением порядка» или «сжать массив, используя O(1) дополнительной памяти». Решение на нём обычно укладывается в O(n). - удалить дубликаты на месте, сохранив порядок - переместить/разбить элементы с сохранением порядка - сжать массив, используя O(1) дополнительной памяти - сдвинуть нули или заданное значение в конец ## Два указателя (в одну сторону): с чем путают? Паттерн «Два указателя (в одну сторону)» чаще всего путают с этими паттернами: Преобразование массива на месте, Два указателя (с концов), Скользящее окно (переменное) и Быстрый и медленный указатели. - **Преобразование массива на месте** - Работа в одном буфере это цель. Отстающий указатель записи это один из способов её достичь. - **Два указателя (с концов)** - Те указатели стартуют по краям отсортированных данных и сходятся. Эти оба стартуют с нулевого индекса. - **Скользящее окно (переменное)** - Окно тоже двигает два индекса вперёд, но хранит живой отрезок. Здесь отстающий индекс это место записи. - **Быстрый и медленный указатели** - Там быстрый указатель делает два шага и ловит цикл. Здесь он делает один и читает. ## Два указателя (в одну сторону): сложность по времени и памяти У паттерна «Два указателя (в одну сторону)» целевая сложность O(n). n до 1e6 даёт O(n) времени и O(1) памяти. Указатель чтения видит каждый элемент один раз. ## Два указателя (в одну сторону): разбор примера ### Оставить каждое значение не более двух раз В отсортированном массиве значение может повторяться много раз. Обрежьте его так, чтобы каждое встречалось максимум дважды. Работайте в том же массиве и верните новую длину. Первые два значения проходят всегда, две копии разрешены. Дальше оставляйте arr[read], только если оно отличается от arr[write - 2]. В той ячейке лежит уже записанная вторая копия. ```javascript function removeDuplicatesII(nums) { let write = 0; for (let read = 0; read < nums.length; read++) { // nums[write - 2] is the copy two slots back in the output if (write < 2 || nums[read] !== nums[write - 2]) { nums[write] = nums[read]; write++; } } return write; // new logical length } ``` ## Два указателя (в одну сторону): частые ошибки - **Дают write обогнать read** Тогда копия затрёт данные, которые read ещё не видел. write двигается только при сохранении. - **Проверяют по исходному массиву** Проверка идёт по arr[write - 1], последнему сохранённому значению. arr[read - 1] может быть уже затёрт. - **Сдвигают хвост при каждом удалении** Линейный проход превращается в O(n в квадрате). Хватает одного копирования вперёд. - **Верят ячейкам после write** В ячейках после write лежат старые значения, оставшиеся до прохода. Смысл имеют только первые write ячеек. ## Два указателя (в одну сторону): задачи с собеседований Паттерн «Два указателя (в одну сторону)» решает такие задачи с собеседований: Удаление дубликатов из отсортированного массива, Удаление элемента, Сдвиг нулей, Удаление дубликатов из отсортированного массива II, Сжатие строки, Является ли подпоследовательностью и Слияние отсортированных массивов. - **Удаление дубликатов из отсортированного массива** Оставляйте значение, только если оно отличается от arr[write - 1]. - **Удаление элемента** Оставляйте всё, что не равно заданному значению. - **Сдвиг нулей** Скопируйте ненулевые вперёд, потом добейте хвост. - **Удаление дубликатов из отсортированного массива II** Проверка смотрит на две ячейки назад, а не на одну. - **Сжатие строки** Запишите символ, потом запишите длину его серии. - **Является ли подпоследовательностью** Один указатель идёт по короткой строке, другой по длинной. - **Слияние отсортированных массивов** Пишите с конца, тогда непрочитанное не затрётся. ## JavaScript ```javascript function compact(arr) { let slow = 0; for (let fast = 0; fast < arr.length; fast++) { if (shouldKeep(arr[fast], arr[slow])) { arr[slow] = arr[fast]; slow++; } } return slow; // new length } ``` ## TypeScript ```typescript declare function shouldKeep(candidate: number, kept: number): boolean; function compact(arr: number[]): number { let slow = 0; for (let fast = 0; fast < arr.length; fast++) { if (shouldKeep(arr[fast], arr[slow])) { arr[slow] = arr[fast]; slow++; } } return slow; // new length } ``` ## Python ```python def compact(arr): slow = 0 for fast in range(len(arr)): if should_keep(arr[fast], arr[slow]): arr[slow] = arr[fast] slow += 1 return slow # new length ``` ## PHP ```php function compact(array &$arr): int { $slow = 0; for ($fast = 0; $fast < count($arr); $fast++) { if (shouldKeep($arr[$fast], $arr[$slow])) { $arr[$slow] = $arr[$fast]; $slow++; } } return $slow; // new length } ``` ## Java ```java public static int compact(int[] arr) { int slow = 0; for (int fast = 0; fast < arr.length; fast++) { if (shouldKeep(arr[fast], arr[slow])) { arr[slow] = arr[fast]; slow++; } } return slow; // new length } private static boolean shouldKeep(int candidate, int kept) { throw new UnsupportedOperationException("your rule here"); } ``` ## C# ```csharp public static int Compact(int[] arr) { int slow = 0; for (int fast = 0; fast < arr.Length; fast++) { if (ShouldKeep(arr[fast], arr[slow])) { arr[slow] = arr[fast]; slow++; } } return slow; // new length } private static bool ShouldKeep(int candidate, int kept) { throw new NotImplementedException("your rule here"); } ``` --- title: "Скользящее окно (фиксированный размер)" url: https://algopath.pro/ru/patterns/sliding-window-fixed language: ru summary: "Окно ровно из k элементов катится по шагу за раз. Добавьте входящее значение, уберите выходящее, остальное не пересчитывайте." updated: 2026-10-17 --- # Скользящее окно (фиксированный размер) Окно ровно из k элементов катится по шагу за раз. Добавьте входящее значение, уберите выходящее, остальное не пересчитывайте. ## Скользящее окно (фиксированный размер): как это работает? Соберите первое окно из первых k элементов. Ответ для него посчитайте один раз, напрямую. Теперь сдвиньте окно на одну ячейку вправо. Ровно один элемент входит и один выходит. Обновите текущее значение по этим двум элементам. Прибавьте вошедший и вычтите вышедший. Запишите ответ для этого окна до следующего сдвига. Лучший результат живёт в одной переменной. Повторяйте, пока правый край не дойдёт до конца. Всего окон n минус k плюс один. Ни одно окно не считается заново целиком. Именно это превращает O(nk) в O(n). Источники: [Википедия: Скользящая средняя](https://ru.wikipedia.org/wiki/%D0%A1%D0%BA%D0%BE%D0%BB%D1%8C%D0%B7%D1%8F%D1%89%D0%B0%D1%8F_%D1%81%D1%80%D0%B5%D0%B4%D0%BD%D1%8F%D1%8F) - `[4, 2, 7] сумма = 13` Размер окна равен трём. Первое окно суммируется напрямую. - `[2, 7, 1] сумма = 10` Входит 1, выходит 4. Значит 13 плюс 1 минус 4 это 10. - `лучшее = 13` Это окно хуже первого. Лучший результат не меняется. - `[7, 1, 5] сумма = 13` Входит 5, выходит 2. Значит 10 плюс 5 минус 2 это 13. - `ответ = 13` Правый край дошёл до конца. Три окна, пять чтений. ## Скользящее окно (фиксированный размер): когда применять? Паттерн «Скользящее окно (фиксированный размер)» нужен, когда в условии есть слова «непрерывный подмассив/подстрока заданной длины k», «каждое окно размера k» или «скользящее среднее или скользящая сумма». Решение на нём обычно укладывается в O(n). - непрерывный подмассив/подстрока заданной длины k - каждое окно размера k - скользящее среднее или скользящая сумма - мин/макс/количество/сумма по каждому фиксированному отрезку ## Скользящее окно (фиксированный размер): с чем путают? Паттерн «Скользящее окно (фиксированный размер)» чаще всего путают с этими паттернами: Скользящее окно (переменное), Префиксные суммы, Монотонная дек-очередь (максимум/минимум окна) и Хеш-множество / словарь. - **Скользящее окно (переменное)** - Переменное окно само выбирает ширину по условию. Это окно на всём проходе шириной k. - **Префиксные суммы** - Префиксные суммы строятся один раз и отвечают про любой диапазон. Окно отвечает про один движущийся отрезок. - **Монотонная дек-очередь (максимум/минимум окна)** - Сумма переживает вычитание, поэтому хватает арифметики. Максимуму нужна дек-очередь. - **Хеш-множество / словарь** - Словарь считает по всей коллекции и не знает позиций. Окно спрашивает только про k соседей. ## Скользящее окно (фиксированный размер): сложность по времени и памяти У паттерна «Скользящее окно (фиксированный размер)» целевая сложность O(n). n до 1e6 при окне размера k даёт O(n). Каждый элемент один раз входит в окно и один раз выходит. ## Скользящее окно (фиксированный размер): разбор примера ### Найти все анаграммы слова Даны строка s и более короткая строка p. Верните все начальные индексы, где в s стоит анаграмма p. Анаграмма использует те же буквы в тех же количествах. Посчитайте буквы p один раз. Потом катите окно такой же длины по строке s. Каждый сдвиг добавляет одну букву и убирает одну. Сравните счётчики и при совпадении запишите индекс. ```javascript function findAnagrams(s, p) { if (p.length > s.length) return []; const need = new Array(26).fill(0); const have = new Array(26).fill(0); const at = (c) => c.charCodeAt(0) - 97; for (const c of p) need[at(c)]++; const result = []; for (let i = 0; i < s.length; i++) { have[at(s[i])]++; // one letter leaves as soon as the window is longer than p if (i >= p.length) have[at(s[i - p.length])]--; if (i >= p.length - 1 && need.every((n, j) => n === have[j])) { result.push(i - p.length + 1); } } return result; } ``` ## Скользящее окно (фиксированный размер): частые ошибки - **Пересобирают окно на каждом шаге** Суммирование k элементов в каждой позиции стоит O(nk). Используйте прошлый итог. - **Записывают ответ слишком рано** Первые k минус одна позиций держат неполное окно. Начинайте запись, когда оно заполнилось. - **Убирают не тот элемент** Выходящее значение стоит на индексе i минус k. Ошибка на единицу сдвигает все окна. - **Берут скользящую сумму ради максимума** Вычитание вышедшего элемента максимум не восстановит. Для этого случая нужна монотонная дек-очередь. ## Скользящее окно (фиксированный размер): задачи с собеседований Паттерн «Скользящее окно (фиксированный размер)» решает такие задачи с собеседований: Максимальная сумма подмассива длины k, Максимальное среднее подмассива I, Все анаграммы в строке, Перестановка в строке, Повторяющиеся последовательности ДНК, Максимум скользящего окна и Средние в радиусе k. - **Максимальная сумма подмассива длины k** Базовая форма: одно сложение и одно вычитание за шаг. - **Максимальное среднее подмассива I** Та же сумма, делённая на k в конце. - **Все анаграммы в строке** Окно несёт счётчики букв вместо суммы. - **Перестановка в строке** Те же счётчики, но останов на первом совпадении. - **Повторяющиеся последовательности ДНК** Каждое окно из десяти символов, подсчёт в словаре. - **Максимум скользящего окна** Фиксированное окно, чей ответ требует монотонной дек-очереди. - **Средние в радиусе k** Окно центрируется на индексе, а не тянется за ним. ## JavaScript ```javascript function windowStat(arr, k) { let windowSum = 0; for (let i = 0; i < k; i++) windowSum += arr[i]; let best = windowSum; for (let i = k; i < arr.length; i++) { windowSum += arr[i] - arr[i - k]; best = Math.max(best, windowSum); } return best; } ``` ## TypeScript ```typescript function windowStat(arr: number[], k: number): number { let windowSum = 0; for (let i = 0; i < k; i++) windowSum += arr[i]; let best = windowSum; for (let i = k; i < arr.length; i++) { windowSum += arr[i] - arr[i - k]; best = Math.max(best, windowSum); } return best; } ``` ## Python ```python def window_stat(arr, k): window_sum = sum(arr[:k]) best = window_sum for i in range(k, len(arr)): window_sum += arr[i] - arr[i - k] best = max(best, window_sum) return best ``` ## PHP ```php function windowStat(array $arr, int $k): int { $windowSum = array_sum(array_slice($arr, 0, $k)); $best = $windowSum; for ($i = $k; $i < count($arr); $i++) { $windowSum += $arr[$i] - $arr[$i - $k]; $best = max($best, $windowSum); } return $best; } ``` ## Java ```java public static int windowStat(int[] arr, int k) { int windowSum = 0; for (int i = 0; i < k; i++) windowSum += arr[i]; int best = windowSum; for (int i = k; i < arr.length; i++) { windowSum += arr[i] - arr[i - k]; best = Math.max(best, windowSum); } return best; } ``` ## C# ```csharp public static int WindowStat(int[] arr, int k) { int windowSum = 0; for (int i = 0; i < k; i++) windowSum += arr[i]; int best = windowSum; for (int i = k; i < arr.Length; i++) { windowSum += arr[i] - arr[i - k]; best = Math.Max(best, windowSum); } return best; } ``` --- title: "Скользящее окно (переменное)" url: https://algopath.pro/ru/patterns/sliding-window-variable language: ru summary: "Растите окно справа, а как только условие сломалось, сужайте его слева. Ни один из концов никогда не идёт назад по массиву." updated: 2026-10-15 --- # Скользящее окно (переменное) Растите окно справа, а как только условие сломалось, сужайте его слева. Ни один из концов никогда не идёт назад по массиву. ## Скользящее окно (переменное): как это работает? Окно начинается пустым, оба конца на нулевом индексе. Счётчик или словарь описывает его содержимое. Правый конец делает шаг и вбирает один элемент. Обновите описание этим элементом. Проверьте условие на текущем окне. Пока оно выполняется, записывайте ширину или количество. Когда условие сломалось, шаг делает левый конец. Уберите этот элемент из описания. Сужайте, пока условие снова не выполнится. Только после этого правый конец идёт дальше. Ни один конец не шагает назад. Поэтому каждый элемент один раз входит и один раз выходит. - `окно = a` Ищем самый длинный отрезок без повторов. Лучшее равно 1. - `окно = ab` Входит b, буква новая. Лучшее становится 2. - `окно = abc` Входит c, буква новая. Лучшее становится 3. - `окно = abcb` Входит вторая b. Теперь в окне две буквы b. - `окно = cb` Левый конец убирает a и первую b. Лучшее остаётся 3. ## Скользящее окно (переменное): когда применять? Паттерн «Скользящее окно (переменное)» нужен, когда в условии есть слова «самый длинный/короткий непрерывный подмассив или подстрока», «удовлетворяющий условию (сумма <= K, не более, без нарушения лимита)» или «не более K различных, без повторов, содержит все из X». Решение на нём обычно укладывается в O(n). - самый длинный/короткий непрерывный подмассив или подстрока - удовлетворяющий условию (сумма <= K, не более, без нарушения лимита) - не более K различных, без повторов, содержит все из X - непрерывный отрезок, не подпоследовательность ## Скользящее окно (переменное): с чем путают? Паттерн «Скользящее окно (переменное)» чаще всего путают с этими паттернами: Скользящее окно (фиксированный размер), Два указателя (с концов), Префиксные суммы и Хеш-множество / словарь. - **Скользящее окно (фиксированный размер)** - Фиксированное окно шириной k от первого шага до последнего. Это окно выбирает ширину само. - **Два указателя (с концов)** - Те стартуют врозь на отсортированных данных и сходятся к паре. Здесь оба конца идут в одну сторону. - **Префиксные суммы** - Префиксные суммы работают с отрицательными числами и любыми диапазонами. Сужение же считает, что удаление помогает. - **Хеш-множество / словарь** - Словарь сам по себе описывает всю коллекцию. Окну нужны счётчики только для живого отрезка. ## Скользящее окно (переменное): сложность по времени и памяти У паттерна «Скользящее окно (переменное)» целевая сложность O(n). n до 1e6 и один лучший отрезок дают O(n). Каждый конец сдвигается не более n раз. ## Скользящее окно (переменное): разбор примера ### Самый длинный отрезок с k различными символами Даны строка и число k. Найдите самый длинный отрезок, где не больше k различных символов. Отрезок должен быть непрерывным, а не разбросанным. Растите окно вправо и считайте символы в словаре. Когда ключей стало больше k, сужайте слева, пока их снова не станет достаточно мало. Ширину меряйте после каждого сужения. ```javascript function longestKDistinct(s, k) { const count = new Map(); let left = 0; let best = 0; for (let right = 0; right < s.length; right++) { count.set(s[right], (count.get(s[right]) ?? 0) + 1); while (count.size > k) { const out = s[left]; count.set(out, count.get(out) - 1); if (count.get(out) === 0) count.delete(out); // the key must go, not just the count left++; } best = Math.max(best, right - left + 1); } return best; } ``` ## Скользящее окно (переменное): частые ошибки - **Оставляют в словаре нулевой счётчик** Размер словаря считает и ключ с нулём. Удаляйте ключ, когда счётчик обнулился. - **Сужают через if вместо while** Одного удаления может не хватить, чтобы вернуть условие. Сужайте, пока оно не выполнится. - **Берут окно там, где числа отрицательные** Удаление значения слева может увеличить сумму. Тогда сужение ничего не доказывает. - **Неверно меряют ширину** Ширина это right минус left плюс один. Без единицы теряется один символ. ## Скользящее окно (переменное): задачи с собеседований Паттерн «Скользящее окно (переменное)» решает такие задачи с собеседований: Самая длинная подстрока без повторов, Минимальная длина подмассива с суммой, Подстрока с не более чем k различными символами, Замена повторяющихся символов, Минимальное окно-подстрока, Фрукты в корзины и Максимум подряд идущих единиц III. - **Самая длинная подстрока без повторов** Сужайте, пока повторяющаяся буква не уйдёт. - **Минимальная длина подмассива с суммой** Растите, пока суммы хватает, потом сужайте. - **Подстрока с не более чем k различными символами** Условием служит размер словаря. - **Замена повторяющихся символов** Ширина минус самый частый счётчик должна остаться меньше k. - **Минимальное окно-подстрока** Растите, пока покрыты все нужные буквы, потом поджимайте. - **Фрукты в корзины** Не более двух видов, та же форма, что и k различных. - **Максимум подряд идущих единиц III** Сужайте, когда в окне стало больше k нулей. ## JavaScript ```javascript function longestUnder(arr, limit) { let left = 0; let sum = 0; let best = 0; for (let right = 0; right < arr.length; right++) { sum += arr[right]; while (sum > limit) { sum -= arr[left]; left++; } best = Math.max(best, right - left + 1); } return best; } ``` ## TypeScript ```typescript function longestUnder(arr: number[], limit: number): number { let left = 0; let sum = 0; let best = 0; for (let right = 0; right < arr.length; right++) { sum += arr[right]; while (sum > limit) { sum -= arr[left]; left++; } best = Math.max(best, right - left + 1); } return best; } ``` ## Python ```python def longest_under(arr, limit): left = 0 total = 0 best = 0 for right, x in enumerate(arr): total += x while total > limit: total -= arr[left] left += 1 best = max(best, right - left + 1) return best ``` ## PHP ```php function longestUnder(array $arr, int $limit): int { $left = 0; $sum = 0; $best = 0; for ($right = 0; $right < count($arr); $right++) { $sum += $arr[$right]; while ($sum > $limit) { $sum -= $arr[$left]; $left++; } $best = max($best, $right - $left + 1); } return $best; } ``` ## Java ```java public static int longestUnder(int[] arr, int limit) { int left = 0; int sum = 0; int best = 0; for (int right = 0; right < arr.length; right++) { sum += arr[right]; while (sum > limit) { sum -= arr[left]; left++; } best = Math.max(best, right - left + 1); } return best; } ``` ## C# ```csharp public static int LongestUnder(int[] arr, int limit) { int left = 0; int sum = 0; int best = 0; for (int right = 0; right < arr.Length; right++) { sum += arr[right]; while (sum > limit) { sum -= arr[left]; left++; } best = Math.Max(best, right - left + 1); } return best; } ``` --- title: "Префиксные суммы" url: https://algopath.pro/ru/patterns/prefix-sums language: ru summary: "Храните накопленную сумму всего, что стоит до данного индекса. Тогда сумма на отрезке это одно вычитание, а не целый цикл." updated: 2026-10-17 --- # Префиксные суммы Храните накопленную сумму всего, что стоит до данного индекса. Тогда сумма на отрезке это одно вычитание, а не целый цикл. ## Префиксные суммы: как это работает? Заведите массив на одну ячейку длиннее входа. Нулевая ячейка это сумма ничего, поэтому там ноль. Пройдите вход слева направо. Каждая ячейка равна прошлой сумме плюс текущее значение. Теперь prefix[i] это сумма первых i значений. Всё начиная с индекса i не входит. Отрезок от l до r это весь префикс до r. Вычтите из него часть до l. Значит ответ равен prefix[r + 1] минус prefix[l]. Это одно вычитание при любой ширине. Массив строится один раз и читается много раз. Он перестаёт быть верным при изменении значения. Источники: [Википедия: Префиксная сумма](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D1%84%D0%B8%D0%BA%D1%81%D0%BD%D0%B0%D1%8F_%D1%81%D1%83%D0%BC%D0%BC%D0%B0) - `prefix = [0]` Вход это [3, 1, 4, 1]. Нулевая ячейка это сумма ничего. - `prefix = [0, 3]` Прибавляем первое значение. Один элемент даёт 3. - `prefix = [0, 3, 4]` Прибавляем 1 к сумме. Два элемента дают 4. - `prefix = [0, 3, 4, 8, 9]` Остальные значения достраивают массив. Построение заняло один проход. - `сумма(1..2) = 8 - 3 = 5` На этом отрезке стоят 1 и 4. Одно вычитание, без цикла. ## Префиксные суммы: когда применять? Паттерн «Префиксные суммы» нужен, когда в условии есть слова «много запросов на сумму по диапазону», «ответить на несколько вопросов о сумме диапазона в одном массиве» или «сумма от индекса l до r». Решение на нём обычно укладывается в O(n) build / O(1) query. - много запросов на сумму по диапазону - ответить на несколько вопросов о сумме диапазона в одном массиве - сумма от индекса l до r - накопленная или кумулятивная сумма ## Префиксные суммы: с чем путают? Паттерн «Префиксные суммы» чаще всего путают с этими паттернами: Скользящее окно (фиксированный размер), Массив разностей, Дерево Фенвика / дерево отрезков и Хеш-множество / словарь. - **Скользящее окно (фиксированный размер)** - Окно отвечает про один движущийся отрезок. Префиксные суммы отвечают про любой названный отрезок. - **Массив разностей** - Это зеркальный случай: много записей на отрезках, одно чтение в конце. Здесь наоборот, много чтений. - **Дерево Фенвика / дерево отрезков** - Префиксный массив ломается при первом же изменении значения. Дерево Фенвика платит log n за право менять. - **Хеш-множество / словарь** - У словаря нет понятия отрезка вообще. Для подсчёта подмассивов рядом нужна накопленная сумма. ## Префиксные суммы: сложность по времени и памяти У паттерна «Префиксные суммы» целевая сложность O(n) build / O(1) query. n до 1e6 и много запросов на отрезках дают O(n) на подготовку. Каждый запрос стоит O(1), плюс O(n) памяти. ## Префиксные суммы: разбор примера ### Сколько подмассивов дают сумму k Дан массив целых чисел и число k. Посчитайте непрерывные отрезки с суммой ровно k. Значения бывают отрицательными, поэтому окно сужать нельзя. Идите по массиву и держите накопленную сумму. Отрезок, кончающийся здесь, даёт k, если какой-то ранний префикс равен сумме минус k. Словарь префиксов их считает. ```javascript function subarraySum(nums, k) { const seen = new Map([[0, 1]]); // one empty prefix, total zero let total = 0; let count = 0; for (const x of nums) { total += x; // an earlier prefix of total - k closes a stretch that sums to k count += seen.get(total - k) ?? 0; seen.set(total, (seen.get(total) ?? 0) + 1); } return count; } ``` ## Префиксные суммы: частые ошибки - **Пропускают ведущий ноль** Тогда отрезку с нулевого индекса нечего вычитать. Массиву нужно n плюс одна ячейка. - **Ошибаются на правом крае** Сумма от l до r читает prefix[r + 1]. Чтение prefix[r] теряет последнее значение. - **Меняют значение после построения** Одна правка ломает все ячейки после неё. При смешанных изменениях берите дерево Фенвика. - **Дают сумме переполниться** Миллион значений около 1e9 выходят за безопасный диапазон. Возьмите BigInt или более широкий тип. ## Префиксные суммы: задачи с собеседований Паттерн «Префиксные суммы» решает такие задачи с собеседований: Сумма на отрезке без изменений, Подмассив с суммой k, Индекс равновесия, Произведение массива кроме себя, Непрерывный подмассив с суммой, Самый длинный подмассив с суммой k и Сумма на прямоугольнике. - **Сумма на отрезке без изменений** Постройте один раз, потом отвечайте одним вычитанием. - **Подмассив с суммой k** Совместите накопленную сумму со словарём ранних префиксов. - **Индекс равновесия** Левая сумма равна общей сумме минус левая сумма минус значение. - **Произведение массива кроме себя** Та же идея, только с префиксными и суффиксными произведениями. - **Непрерывный подмассив с суммой** Храните префиксы по модулю k вместо самих сумм. - **Самый длинный подмассив с суммой k** Запоминайте первый индекс, где встретился каждый префикс. - **Сумма на прямоугольнике** Два измерения, четыре чтения на запрос. ## JavaScript ```javascript function buildPrefix(arr) { const prefix = new Array(arr.length + 1).fill(0); for (let i = 0; i < arr.length; i++) { prefix[i + 1] = prefix[i] + arr[i]; } return prefix; } function rangeSum(prefix, l, r) { return prefix[r + 1] - prefix[l]; // sum of arr[l..r] } ``` ## TypeScript ```typescript function buildPrefix(arr: number[]): number[] { const prefix: number[] = new Array(arr.length + 1).fill(0); for (let i = 0; i < arr.length; i++) { prefix[i + 1] = prefix[i] + arr[i]; } return prefix; } function rangeSum(prefix: number[], l: number, r: number): number { return prefix[r + 1] - prefix[l]; // sum of arr[l..r] } ``` ## Python ```python def build_prefix(arr): prefix = [0] * (len(arr) + 1) for i, val in enumerate(arr): prefix[i + 1] = prefix[i] + val return prefix def range_sum(prefix, l, r): return prefix[r + 1] - prefix[l] # sum of arr[l..r] ``` ## PHP ```php function buildPrefix(array $arr): array { $prefix = array_fill(0, count($arr) + 1, 0); foreach ($arr as $i => $val) { $prefix[$i + 1] = $prefix[$i] + $val; } return $prefix; } function rangeSum(array $prefix, int $l, int $r): int { return $prefix[$r + 1] - $prefix[$l]; // sum of arr[l..r] } ``` ## Java ```java public static int[] buildPrefix(int[] arr) { int[] prefix = new int[arr.length + 1]; for (int i = 0; i < arr.length; i++) { prefix[i + 1] = prefix[i] + arr[i]; } return prefix; } public static int rangeSum(int[] prefix, int l, int r) { return prefix[r + 1] - prefix[l]; // sum of arr[l..r] } ``` ## C# ```csharp public static int[] BuildPrefix(int[] arr) { var prefix = new int[arr.Length + 1]; for (int i = 0; i < arr.Length; i++) { prefix[i + 1] = prefix[i] + arr[i]; } return prefix; } public static int RangeSum(int[] prefix, int l, int r) { return prefix[r + 1] - prefix[l]; // sum of arr[l..r] } ``` --- title: "Массив разностей" url: https://algopath.pro/ru/patterns/difference-array language: ru summary: "Записывайте не значение, а изменение в каждой точке. Обновление отрезка стоит двух записей, а один проход в конце всё собирает." updated: 2026-10-17 --- # Массив разностей Записывайте не значение, а изменение в каждой точке. Обновление отрезка стоит двух записей, а один проход в конце всё собирает. ## Массив разностей: как это работает? Заведите массив нулей на одну ячейку длиннее входа. Каждая ячейка означает изменение в этой точке. Чтобы прибавить v на отрезке от l до r, сделайте две записи. Прибавьте v в l и вычтите v в r плюс один. Эти две записи и есть всё обновление. Ничего между l и r не трогается. Повторите для каждого заданного отрезка. Каждый стоит двух записей при любой ширине. Когда обновления кончились, пройдите массив с накоплением. Тогда ячейка i хранит настоящее значение. Сборка это один проход. Значит m обновлений на n ячейках стоят O(n + m). Источники: [Википедия: Префиксная сумма](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D1%84%D0%B8%D0%BA%D1%81%D0%BD%D0%B0%D1%8F_%D1%81%D1%83%D0%BC%D0%BC%D0%B0) - `diff = [0, 0, 0, 0, 0, 0]` Пять рабочих ячеек плюс одна запасная. Всё начинается с нулей. - `diff = [0, 2, 0, 0, -2, 0]` Прибавляем 2 на отрезке с 1 по 3. Две записи, а не три. - `diff = [3, 2, -3, 0, -2, 0]` Прибавляем 3 на отрезке с 0 по 1. Снова две записи. - `накопление = [3, 5, 2, 2, 0]` Один проход с накоплением восстанавливает значения. - `ответ = [3, 5, 2, 2, 0]` Два обновления стоили четырёх записей. Ширина не имела значения. ## Массив разностей: когда применять? Паттерн «Массив разностей» нужен, когда в условии есть слова «сначала много обновлений диапазонов, затем один раз прочитать итоговый массив», «прибавить значение к каждому элементу диапазона, повторить много раз» или «применить k приращений диапазона перед тем как отвечать на запросы». Решение на нём обычно укладывается в O(n). - сначала много обновлений диапазонов, затем один раз прочитать итоговый массив - прибавить значение к каждому элементу диапазона, повторить много раз - применить k приращений диапазона перед тем как отвечать на запросы - счётчики бронирований/интервалов по массиву ## Массив разностей: с чем путают? Паттерн «Массив разностей» чаще всего путают с этими паттернами: Префиксные суммы, Линия развёртки (подсчёт событий), Дерево Фенвика / дерево отрезков и Скользящее окно (фиксированный размер). - **Префиксные суммы** - Префиксные суммы читают много отрезков из неизменных данных. Здесь зеркально: много записей, одно чтение. - **Линия развёртки (подсчёт событий)** - Развёртка сортирует события, когда координаты огромны или разрежены. Здесь они прямо индексируют массив. - **Дерево Фенвика / дерево отрезков** - Дерево Фенвика отвечает на запросы между обновлениями. Массив разностей отвечает только после последнего. - **Скользящее окно (фиксированный размер)** - Окно ведёт один отрезок постоянной ширины. Здесь отрезки разной ширины и свободно накладываются. ## Массив разностей: сложность по времени и памяти У паттерна «Массив разностей» целевая сложность O(n). n до 1e6 и m обновлений на отрезках дают O(n + m). Обновление это две записи, сборка это один проход. ## Массив разностей: разбор примера ### Забронированные места на рейсах У вас n рейсов и список броней. Каждая бронь добавляет места всем рейсам в диапазоне. Верните общее число забронированных мест на каждом рейсе. Цикл на каждую бронь стоил бы O(n) при широком диапазоне. Вместо этого запишите каждую бронь двумя числами. Накопление сделайте один раз, когда все брони учтены. ```javascript function corpFlightBookings(bookings, n) { const diff = new Array(n + 1).fill(0); for (const [first, last, seats] of bookings) { diff[first - 1] += seats; diff[last] -= seats; // one slot past the end of the range } const answer = new Array(n); let running = 0; for (let i = 0; i < n; i++) { running += diff[i]; answer[i] = running; } return answer; } ``` ## Массив разностей: частые ошибки - **Берут массив длиной ровно n** Отрезок, кончающийся на последнем индексе, пишет в ячейку n. Выделяйте n плюс одну. - **Вычитают в r, а не в r плюс один** Правый край входит в отрезок, поэтому изменение должно его застать. Вычитайте на ячейку позже. - **Читают значение до сборки** До финального прохода в ячейках лежат изменения, а не значения. Спрашивайте только после него. - **Пишут одиночное значение напрямую** Один индекс это отрезок шириной один. Ему тоже нужны обе записи. ## Массив разностей: задачи с собеседований Паттерн «Массив разностей» решает такие задачи с собеседований: Брони на рейсы, Прибавление на отрезке, Совместные поездки, Сдвиг букв II, Год максимального населения, Переговорные комнаты II и Сколько цветов цветёт. - **Брони на рейсы** Каждая бронь это две записи на диапазоне рейсов. - **Прибавление на отрезке** Чистая форма приёма, без всего остального. - **Совместные поездки** Пассажиры садятся и выходят, а лимит нарушать нельзя. - **Сдвиг букв II** Каждый сдвиг покрывает диапазон позиций в строке. - **Год максимального населения** Рождение прибавляет единицу, смерть вычитает. - **Переговорные комнаты II** Начало занимает комнату, конец возвращает её. - **Сколько цветов цветёт** Каждый цветок покрывает диапазон дней. ## JavaScript ```javascript function applyRangeUpdates(n, updates) { const diff = new Array(n + 1).fill(0); for (const [l, r, val] of updates) { diff[l] += val; diff[r + 1] -= val; } const result = new Array(n); let running = 0; for (let i = 0; i < n; i++) { running += diff[i]; result[i] = running; } return result; } ``` ## TypeScript ```typescript function applyRangeUpdates(n: number, updates: [number, number, number][]): number[] { const diff: number[] = new Array(n + 1).fill(0); for (const [l, r, val] of updates) { diff[l] += val; diff[r + 1] -= val; } const result: number[] = new Array(n); let running = 0; for (let i = 0; i < n; i++) { running += diff[i]; result[i] = running; } return result; } ``` ## Python ```python def apply_range_updates(n, updates): diff = [0] * (n + 1) for l, r, val in updates: diff[l] += val diff[r + 1] -= val result = [0] * n running = 0 for i in range(n): running += diff[i] result[i] = running return result ``` ## PHP ```php function applyRangeUpdates(int $n, array $updates): array { $diff = array_fill(0, $n + 1, 0); foreach ($updates as [$l, $r, $val]) { $diff[$l] += $val; $diff[$r + 1] -= $val; } $result = array_fill(0, $n, 0); $running = 0; for ($i = 0; $i < $n; $i++) { $running += $diff[$i]; $result[$i] = $running; } return $result; } ``` ## Java ```java public static int[] applyRangeUpdates(int n, int[][] updates) { int[] diff = new int[n + 1]; for (int[] u : updates) { diff[u[0]] += u[2]; diff[u[1] + 1] -= u[2]; } int[] result = new int[n]; int running = 0; for (int i = 0; i < n; i++) { running += diff[i]; result[i] = running; } return result; } ``` ## C# ```csharp public static int[] ApplyRangeUpdates(int n, int[][] updates) { var diff = new int[n + 1]; foreach (var u in updates) { diff[u[0]] += u[2]; diff[u[1] + 1] -= u[2]; } var result = new int[n]; int running = 0; for (int i = 0; i < n; i++) { running += diff[i]; result[i] = running; } return result; } ``` --- title: "Линейный поиск" url: https://algopath.pro/ru/patterns/linear-search language: ru summary: "Идите по коллекции с одного конца и остановитесь на первом подходящем элементе. Ни порядка, ни структуры, ни подготовки." updated: 2026-10-17 --- # Линейный поиск Идите по коллекции с одного конца и остановитесь на первом подходящем элементе. Ни порядка, ни структуры, ни подготовки. ## Линейный поиск: как это работает? Начните с первого элемента коллекции. Спросите, тот ли это элемент. Если он подошёл, остановитесь и верните его индекс. Смотреть дальше незачем. Если не подошёл, перейдите к следующему. Задайте тот же вопрос снова. Если конец наступил без совпадения, сообщите о неудаче. Обычно возвращают минус один или null. Про данные не предполагается вообще ничего. Они могут быть отсортированы, перемешаны или ещё поступать. Худший случай читает каждый элемент один раз. Лучший читает ровно один. Источники: [Википедия: Линейный поиск](https://ru.wikipedia.org/wiki/%D0%9B%D0%B8%D0%BD%D0%B5%D0%B9%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA) - `i = 0, значение 5` Ищем 2 в [5, 8, 2, 9]. Первый элемент мимо. - `i = 1, значение 8` Совпадения по-прежнему нет. Идём к следующему индексу. - `i = 2, значение 2` Этот элемент равен искомому. - `возврат 2` Индекс уходит сразу. Девятку никто не читал. - `цель 7 даёт -1` Отсутствующее значение стоит целого прохода. Это худший случай. ## Линейный поиск: когда применять? Паттерн «Линейный поиск» нужен, когда в условии есть слова «неотсортированный массив», «проверить каждый элемент» или «нет гарантии порядка». Решение на нём обычно укладывается в O(n). - неотсортированный массив - проверить каждый элемент - нет гарантии порядка - найти первое/любое совпадение - малый n или разовый проход ## Линейный поиск: с чем путают? Паттерн «Линейный поиск» чаще всего путают с этими паттернами: Бинарный поиск (массив), Хеш-множество / словарь, Два указателя (в одну сторону), Скользящее окно (переменное) и Элементарные сортировки (выбором, пузырьком, вставками). - **Бинарный поиск (массив)** - Бинарному поиску нужны отсортированные данные, зато он платит log n. Перебор читает что угодно. - **Хеш-множество / словарь** - Словарь отвечает мгновенно, но его надо сначала построить. Ради одного поиска перебор дешевле. - **Два указателя (в одну сторону)** - Та пара переписывает массив прямо во время прохода. Поиск только читает. - **Скользящее окно (переменное)** - Окно несёт описание живого отрезка. Перебор несёт максимум лучшее из увиденного. - **Элементарные сортировки (выбором, пузырьком, вставками)** - Сортировка стоит O(n log n) и окупается при многих поисках. Один поиск её не окупает. ## Линейный поиск: сложность по времени и памяти У паттерна «Линейный поиск» целевая сложность O(n). n примерно до 1e7 даёт O(n) времени и O(1) памяти. Больше или многократно, значит нужна структура. ## Линейный поиск: разбор примера ### Первый неповторяющийся символ Дана строка. Верните индекс первого символа, который встречается ровно один раз. Если все символы повторяются, верните минус один. Посчитайте все символы за один проход. Хватит словаря с символом в ключе. Потом пройдите строку слева ещё раз. Верните первый индекс со счётчиком один. ```javascript function firstUniqChar(s) { const count = new Map(); for (const c of s) { count.set(c, (count.get(c) ?? 0) + 1); } // the second pass is a plain scan: the first hit wins for (let i = 0; i < s.length; i++) { if (count.get(s[i]) === 1) return i; } return -1; } ``` ## Линейный поиск: частые ошибки - **Перебирают внутри другого цикла** Перебор на каждый элемент превращает O(n) в O(n в квадрате). Постройте словарь один раз. - **Возвращают значение вместо индекса** Такие задачи почти всегда просят позицию. Значение не скажет, где оно нашлось. - **Забывают случай без совпадения** Цикл, дошедший до конца, всё равно обязан что-то вернуть. Выберите минус один и держитесь его. - **Перебирают уже отсортированные данные** На отсортированном входе бинарный поиск куда дешевле. Проверьте вход до написания цикла. ## Линейный поиск: задачи с собеседований Паттерн «Линейный поиск» решает такие задачи с собеседований: Линейный поиск, Первый неповторяющийся символ, Поиск максимума, Есть ли дубликаты на крошечном входе, Пропущенное число, Все индексы значения и Мажоритарный элемент. - **Линейный поиск** Чистая форма: остановиться на первом совпадении. - **Первый неповторяющийся символ** Посчитайте один раз, потом ищите счётчик один. - **Поиск максимума** Держите лучшее найденное и сравнивайте с ним каждый элемент. - **Есть ли дубликаты на крошечном входе** Вложенный перебор нормален, когда n очень мало. - **Пропущенное число** Идите и сравнивайте каждый индекс с ожидаемым там значением. - **Все индексы значения** Тот же проход, но без остановки на первом совпадении. - **Мажоритарный элемент** Один проход со счётчиком даёт голосование Бойера-Мура. ## JavaScript ```javascript function linearSearch(arr, target) { for (let i = 0; i < arr.length; i++) { if (arr[i] === target) return i; } return -1; } ``` ## TypeScript ```typescript function linearSearch(arr: number[], target: number): number { for (let i = 0; i < arr.length; i++) { if (arr[i] === target) return i; } return -1; } ``` ## Python ```python def linear_search(arr, target): for i, x in enumerate(arr): if x == target: return i return -1 ``` ## PHP ```php function linearSearch(array $arr, $target) { foreach ($arr as $i => $x) { if ($x === $target) return $i; } return -1; } ``` ## Java ```java public static int linearSearch(int[] arr, int target) { for (int i = 0; i < arr.length; i++) { if (arr[i] == target) return i; } return -1; } ``` ## C# ```csharp public static int LinearSearch(int[] arr, int target) { for (int i = 0; i < arr.Length; i++) { if (arr[i] == target) return i; } return -1; } ``` --- title: "Бинарный поиск (массив)" url: https://algopath.pro/ru/patterns/binary-search-array language: ru summary: "Посмотрите на середину отсортированного отрезка и выбросьте половину, где ответа быть не может. Повторяйте до одного элемента." updated: 2026-10-17 --- # Бинарный поиск (массив) Посмотрите на середину отсортированного отрезка и выбросьте половину, где ответа быть не может. Повторяйте до одного элемента. ## Бинарный поиск (массив): как это работает? Держите две границы, низ и верх, на отсортированном отрезке. Ответ, если он есть, лежит между ними. Возьмите средний индекс этого отрезка. Сравните стоящее там значение с искомым. При совпадении поиск закончен. Верните этот индекс. Если среднее значение мало, цель лежит правее. Сдвиньте низ за середину. Если оно велико, сдвиньте верх левее середины. В обоих случаях половина отрезка исчезает. Цикл кончается, когда низ обогнал верх. Отрезок пуст, значит цели нет. Источники: [Википедия: Двоичный поиск](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA), [Документация Python (англ.): bisect](https://docs.python.org/3/library/bisect.html) - `lo=0 hi=5` Ищем 7 в [1, 3, 5, 7, 9, 11]. Кандидатов шесть. - `mid=2, значение 5` 5 меньше 7. Всё левее выбывает. - `lo=3 hi=5, mid=4, значение 9` 9 больше 7. Всё правее выбывает. - `lo=3 hi=3, mid=3, значение 7` Остался один кандидат, и он подходит. - `возврат 3` Три сравнения на шесть элементов. Удвоение входа добавит одно. ## Бинарный поиск (массив): когда применять? Паттерн «Бинарный поиск (массив)» нужен, когда в условии есть слова «отсортированный массив», «найти цель / первое или последнее вхождение» или «n до 1e6+ и ожидается O(log n)». Решение на нём обычно укладывается в O(log n). - отсортированный массив - найти цель / первое или последнее вхождение - n до 1e6+ и ожидается O(log n) - найти границу, где условие меняется - поиск во вращённом, но почти отсортированном массиве (с подвохом) ## Бинарный поиск (массив): с чем путают? Паттерн «Бинарный поиск (массив)» чаще всего путают с этими паттернами: Линейный поиск, Бинарный поиск во вращённом массиве, Бинарный поиск по ответу, Двоичное дерево поиска и Два указателя (с концов). - **Линейный поиск** - Перебор читает каждый элемент, но порядка не требует. Бинарному поиску порядок нужен, зато он читает log n. - **Бинарный поиск во вращённом массиве** - Поворот рвёт единый отсортированный кусок. Проверка сначала ищет отсортированную половину. - **Бинарный поиск по ответу** - Там отрезок это множество возможных ответов, а не массив. Проверку вы пишете сами. - **Двоичное дерево поиска** - Дерево делит пополам так же, но через указатели. Ещё оно допускает вставки, чего массив не умеет. - **Два указателя (с концов)** - Там двигаются оба конца, и проверяемая пара меняется. Здесь за шаг двигается одна граница. ## Бинарный поиск (массив): сложность по времени и памяти У паттерна «Бинарный поиск (массив)» целевая сложность O(log n). n до 1e9 нормально, потому что log2 от 1e9 около 30. Время O(log n), память O(1). ## Бинарный поиск (массив): разбор примера ### Первая и последняя позиция значения В отсортированном массиве одно значение может повторяться много раз. Верните первый и последний индекс цели. Если значения нет, верните минус один дважды. Запустите поиск дважды с разным правилом при совпадении. Для первого индекса после совпадения двигайте верх влево. Для последнего двигайте низ вправо. ```javascript function searchRange(nums, target) { function bound(findFirst) { let lo = 0; let hi = nums.length - 1; let found = -1; while (lo <= hi) { const mid = lo + Math.floor((hi - lo) / 2); // cannot overflow if (nums[mid] === target) { found = mid; // a match is not the end: keep squeezing the chosen side if (findFirst) hi = mid - 1; else lo = mid + 1; } else if (nums[mid] < target) { lo = mid + 1; } else { hi = mid - 1; } } return found; } return [bound(true), bound(false)]; } ``` ## Бинарный поиск (массив): частые ошибки - **Считают mid как lo плюс hi пополам** Такая сумма может выйти за безопасный диапазон. Пишите lo плюс половина разрыва. - **Двигают границу в mid, а не за него** Отрезок перестаёт сжиматься, и цикл не кончается. Берите mid плюс один или mid минус один. - **Смешивают два вида цикла** При lo <= hi обе границы входят в отрезок. Смена одной границы без другой ломает инвариант. - **Ищут в неотсортированных данных** Сравнение считает, что слева от mid всё меньше. Без этого ответ тихо неверен. ## Бинарный поиск (массив): задачи с собеседований Паттерн «Бинарный поиск (массив)» решает такие задачи с собеседований: Бинарный поиск, Позиция для вставки, Первая и последняя позиция элемента, Поиск пика, Квадратный корень, Поиск в двумерной матрице и Наименьшая буква больше заданной. - **Бинарный поиск** Чистая форма на различных отсортированных значениях. - **Позиция для вставки** Верните место, куда значение встало бы, если его нет. - **Первая и последняя позиция элемента** Два поиска с противоположным правилом при совпадении. - **Поиск пика** Сравнение идёт с соседом, а не с целью. - **Квадратный корень** Отрезок это множество ответов, а не массив. - **Поиск в двумерной матрице** Считайте всю сетку одним отсортированным списком. - **Наименьшая буква больше заданной** Верхняя граница, которая заворачивается к началу. ## JavaScript ```javascript function binarySearch(arr, target) { let lo = 0, hi = arr.length - 1; while (lo <= hi) { const mid = (lo + hi) >> 1; if (arr[mid] === target) return mid; if (arr[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; } ``` ## TypeScript ```typescript function binarySearch(arr: number[], target: number): number { let lo = 0, hi = arr.length - 1; while (lo <= hi) { const mid = (lo + hi) >> 1; if (arr[mid] === target) return mid; if (arr[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; } ``` ## Python ```python def binary_search(arr, target): lo, hi = 0, len(arr) - 1 while lo <= hi: mid = (lo + hi) // 2 if arr[mid] == target: return mid if arr[mid] < target: lo = mid + 1 else: hi = mid - 1 return -1 ``` ## PHP ```php function binarySearch(array $arr, $target) { $lo = 0; $hi = count($arr) - 1; while ($lo <= $hi) { $mid = intdiv($lo + $hi, 2); if ($arr[$mid] === $target) return $mid; if ($arr[$mid] < $target) $lo = $mid + 1; else $hi = $mid - 1; } return -1; } ``` ## Java ```java public static int binarySearch(int[] arr, int target) { int lo = 0, hi = arr.length - 1; while (lo <= hi) { int mid = (lo + hi) >>> 1; if (arr[mid] == target) return mid; if (arr[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; } ``` ## C# ```csharp public static int BinarySearch(int[] arr, int target) { int lo = 0, hi = arr.Length - 1; while (lo <= hi) { int mid = (int)(((uint)(lo + hi)) >> 1); if (arr[mid] == target) return mid; if (arr[mid] < target) lo = mid + 1; else hi = mid - 1; } return -1; } ``` --- title: "Бинарный поиск во вращённом массиве" url: https://algopath.pro/ru/patterns/binary-search-rotated language: ru summary: "Отсортированный массив, разрезанный и переставленный, на каждом шаге хранит одну отсортированную половину. Найдите её и выберите сторону." updated: 2026-10-17 --- # Бинарный поиск во вращённом массиве Отсортированный массив, разрезанный и переставленный, на каждом шаге хранит одну отсортированную половину. Найдите её и выберите сторону. ## Бинарный поиск во вращённом массиве: как это работает? Вращённый массив это один отсортированный кусок, разрезанный и переставленный. Каждая из двух частей внутри отсортирована. Возьмите средний индекс как обычно. Сравните значение там со значением на нижней границе. Если нижнее значение не больше, отсортирована левая половина. Иначе отсортирована правая. Теперь одна половина полностью понятна. Её концы ограничивают все значения внутри. Проверьте, попадает ли цель между этими концами. Если да, ищите в этой половине, иначе в другой. В любом случае половина отрезка исчезает. Стоимость остаётся log n. Источники: [Википедия: Двоичный поиск](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA) - `lo=0 hi=6, mid=3, значение 7` Ищем 0 в массиве [4, 5, 6, 7, 0, 1, 2]. - `левая половина от 4 до 7 отсортирована` Нижнее значение 4, среднее 7. Значит слева переноса нет. - `lo=4 hi=6` 0 не попадает между 4 и 7. Ответ должен быть справа. - `mid=5, значение 1, левая половина от 0 до 1` Левая половина снова отсортирована. На этот раз 0 попадает внутрь. - `lo=4 hi=4, значение 0` Остался один кандидат, и он подходит. Три шага на семь элементов. ## Бинарный поиск во вращённом массиве: когда применять? Паттерн «Бинарный поиск во вращённом массиве» нужен, когда в условии есть слова «отсортированный массив провёрнут в неизвестной точке», «найти цель за O(log n)» или «без дубликатов (или обработать их отдельно)». Решение на нём обычно укладывается в O(log n). - отсортированный массив провёрнут в неизвестной точке - найти цель за O(log n) - без дубликатов (или обработать их отдельно) - найти минимум / точку вращения - массив растёт, а потом один раз падает ## Бинарный поиск во вращённом массиве: с чем путают? Паттерн «Бинарный поиск во вращённом массиве» чаще всего путают с этими паттернами: Бинарный поиск (массив), Линейный поиск, Бинарный поиск по ответу и Быстрая сортировка (merge / quick). - **Бинарный поиск (массив)** - Обычный поиск сравнивает середину с целью. Здесь она сначала сравнивается с границей. - **Линейный поиск** - Перебор справляется с вращённым массивом вообще без рассуждений. Он стоит O(n) вместо O(log n). - **Бинарный поиск по ответу** - Там отрезок состоит из кандидатов в ответы. Здесь вы ищете реально лежащие значения. - **Быстрая сортировка (merge / quick)** - Сортировка вернёт порядок, но стоит n log n. Поворот и так оставляет достаточно порядка. ## Бинарный поиск во вращённом массиве: сложность по времени и памяти У паттерна «Бинарный поиск во вращённом массиве» целевая сложность O(log n). n до 1e9 даёт O(log n), как и обычный поиск. Дубликаты доводят худший случай до O(n). ## Бинарный поиск во вращённом массиве: разбор примера ### Минимум во вращённом массиве Отсортированный массив повернули неизвестное число раз. Найдите его наименьшее значение. Значения различны, и перебирать весь массив нельзя. Сравните среднее значение со значением на верхней границе. Если среднее больше, точка поворота лежит правее. Иначе минимум это середина или что-то левее. ```javascript function findMin(nums) { let lo = 0; let hi = nums.length - 1; while (lo < hi) { const mid = lo + Math.floor((hi - lo) / 2); // compare against the right end, never the left if (nums[mid] > nums[hi]) { lo = mid + 1; // the rotation point is further right } else { hi = mid; // mid may itself be the minimum } } return nums[lo]; } ``` ## Бинарный поиск во вращённом массиве: частые ошибки - **Сначала сравнивают середину с целью** Цель ничего не говорит о том, какая половина отсортирована. Сравнивайте середину с границей. - **Двигают верх за середину при поиске минимума** Средний элемент сам может быть минимумом. Перепрыгнуть его вправе только нижняя граница. - **Считают, что поворот точно был** Поворот на ноль это тоже допустимый вход. Проверка отсортированной половины обязана его покрывать. - **Не учитывают повторяющиеся значения** Когда на обеих границах одно значение, половину не опознать. Безопасно только сузить отрезок на единицу. ## Бинарный поиск во вращённом массиве: задачи с собеседований Паттерн «Бинарный поиск во вращённом массиве» решает такие задачи с собеседований: Поиск во вращённом массиве, Поиск во вращённом массиве II, Минимум во вращённом массиве, Минимум во вращённом массиве II, На сколько повернули массив, Индекс пика в горном массиве и Поиск в горном массиве. - **Поиск во вращённом массиве** Чистая форма, значения различны. - **Поиск во вращённом массиве II** Дубликаты дают линейный худший случай. - **Минимум во вращённом массиве** Сравнение идёт с верхней границей, а не с целью. - **Минимум во вращённом массиве II** То же самое, но случай дубликатов разбирается вручную. - **На сколько повернули массив** Индекс минимума и есть число поворотов. - **Индекс пика в горном массиве** То же деление пополам, но по сравнению с соседом. - **Поиск в горном массиве** Сначала найдите пик, потом ищите на каждой стороне. ## JavaScript ```javascript function searchRotated(arr, target) { let lo = 0, hi = arr.length - 1; while (lo <= hi) { const mid = (lo + hi) >> 1; if (arr[mid] === target) return mid; if (arr[lo] <= arr[mid]) { if (arr[lo] <= target && target < arr[mid]) hi = mid - 1; else lo = mid + 1; } else { if (arr[mid] < target && target <= arr[hi]) lo = mid + 1; else hi = mid - 1; } } return -1; } ``` ## TypeScript ```typescript function searchRotated(arr: number[], target: number): number { let lo = 0, hi = arr.length - 1; while (lo <= hi) { const mid = (lo + hi) >> 1; if (arr[mid] === target) return mid; if (arr[lo] <= arr[mid]) { if (arr[lo] <= target && target < arr[mid]) hi = mid - 1; else lo = mid + 1; } else { if (arr[mid] < target && target <= arr[hi]) lo = mid + 1; else hi = mid - 1; } } return -1; } ``` ## Python ```python def search_rotated(arr, target): lo, hi = 0, len(arr) - 1 while lo <= hi: mid = (lo + hi) // 2 if arr[mid] == target: return mid if arr[lo] <= arr[mid]: if arr[lo] <= target < arr[mid]: hi = mid - 1 else: lo = mid + 1 else: if arr[mid] < target <= arr[hi]: lo = mid + 1 else: hi = mid - 1 return -1 ``` ## PHP ```php function searchRotated(array $arr, $target) { $lo = 0; $hi = count($arr) - 1; while ($lo <= $hi) { $mid = intdiv($lo + $hi, 2); if ($arr[$mid] === $target) return $mid; if ($arr[$lo] <= $arr[$mid]) { if ($arr[$lo] <= $target && $target < $arr[$mid]) $hi = $mid - 1; else $lo = $mid + 1; } else { if ($arr[$mid] < $target && $target <= $arr[$hi]) $lo = $mid + 1; else $hi = $mid - 1; } } return -1; } ``` ## Java ```java public static int searchRotated(int[] arr, int target) { int lo = 0, hi = arr.length - 1; while (lo <= hi) { int mid = (lo + hi) >>> 1; if (arr[mid] == target) return mid; if (arr[lo] <= arr[mid]) { if (arr[lo] <= target && target < arr[mid]) hi = mid - 1; else lo = mid + 1; } else { if (arr[mid] < target && target <= arr[hi]) lo = mid + 1; else hi = mid - 1; } } return -1; } ``` ## C# ```csharp public static int SearchRotated(int[] arr, int target) { int lo = 0, hi = arr.Length - 1; while (lo <= hi) { int mid = (int)(((uint)(lo + hi)) >> 1); if (arr[mid] == target) return mid; if (arr[lo] <= arr[mid]) { if (arr[lo] <= target && target < arr[mid]) hi = mid - 1; else lo = mid + 1; } else { if (arr[mid] < target && target <= arr[hi]) lo = mid + 1; else hi = mid - 1; } } return -1; } ``` --- title: "Бинарный поиск по ответу" url: https://algopath.pro/ru/patterns/binary-search-on-answer language: ru summary: "Искать здесь надо не в данных, а в диапазоне возможных ответов. Проверка да или нет говорит, какую половину можно выбросить." updated: 2026-10-22 --- # Бинарный поиск по ответу Искать здесь надо не в данных, а в диапазоне возможных ответов. Проверка да или нет говорит, какую половину можно выбросить. ## Бинарный поиск по ответу: как это работает? Назовите наименьшее и наибольшее значение, каким ответ может быть. Эта пара и есть диапазон поиска. Напишите проверку, работает ли один кандидат. Она отвечает только да или нет. Проверка обязана быть монотонной: если ответ да, то для больших кандидатов тоже да. Иначе делить пополам нельзя. Возьмите среднего кандидата и запустите проверку. Это значение, а не индекс. При ответе да запомните его и ищите в меньшей половине. При ответе нет ищите в большей. В задачах «максимизировать минимум» всё наоборот: ответ да верен и для всех меньших кандидатов. Тогда ответ да отправляет поиск в большую половину. Диапазон делится пополам каждый раунд. Тридцать раундов покрывают миллиард кандидатов. Источники: [Википедия: Двоичный поиск](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA), [Документация Python (англ.): bisect](https://docs.python.org/3/library/bisect.html) - `диапазон = от 1 до 11` Съесть кучи [3, 6, 7, 11] за 8 часов. Ответ это скорость. - `скорость 6 даёт 6 часов` Шесть часов укладываются в лимит. Меньшая скорость может тоже подойти. - `диапазон = от 1 до 5` Скорость 6 сохранена как лучшая. Теперь пробуем нижнюю половину. - `скорость 3 даёт 10 часов` Это больше лимита. Всё ниже 3 ещё хуже. - `скорость 4 даёт 8 часов` Ровно в лимит, а меньшее не проходит. Ответ равен 4. ## Бинарный поиск по ответу: когда применять? Паттерн «Бинарный поиск по ответу» нужен, когда в условии есть слова «минимизировать максимум / максимизировать минимум», «наименьшее значение, удовлетворяющее условию» или «найти наименьшую вместимость/скорость/число дней, которое подходит». Решение на нём обычно укладывается в O(n log(range)). - минимизировать максимум / максимизировать минимум - наименьшее значение, удовлетворяющее условию - найти наименьшую вместимость/скорость/число дней, которое подходит - ответ лежит в числовом диапазоне, а не в позиции массива - проверка выполнимости монотонна (становится true после некоторой точки) ## Бинарный поиск по ответу: с чем путают? Паттерн «Бинарный поиск по ответу» чаще всего путают с этими паттернами: Бинарный поиск (массив), Жадный алгоритм (обменный аргумент), Линейный поиск и Динамическое программирование (1-D). - **Бинарный поиск (массив)** - Там отрезок состоит из реально лежащих данных. Здесь из всех значений, какими ответ может быть. - **Жадный алгоритм (обменный аргумент)** - Сама проверка часто и есть жадный алгоритм. Поиск лишь выбирает, какого кандидата проверить. - **Линейный поиск** - Перебирать кандидатов подряд верно, но долго. Монотонная проверка позволяет отбросить половину. - **Динамическое программирование (1-D)** - DP собирает ответ из ответов поменьше. Здесь ответ угадывается и затем проверяется. ## Бинарный поиск по ответу: сложность по времени и памяти У паттерна «Бинарный поиск по ответу» целевая сложность O(n log(range)). Диапазон в 1e9 требует около 30 проверок. Итог O(n log range), если одна проверка стоит O(n). ## Бинарный поиск по ответу: разбор примера ### Отгрузить все посылки за d дней Посылки отгружаются в заданном порядке за d дней. Выберите наименьшую дневную вместимость, при которой всё успевает. Одну посылку нельзя разделить между двумя днями. Ответ лежит между самой большой посылкой и суммой всех. Для одного кандидата заполняйте дни жадно и считайте дни. Сравните это число с d. ```javascript function shipWithinDays(weights, days) { let lo = Math.max(...weights); // one day must hold the biggest package let hi = weights.reduce((a, b) => a + b, 0); // one day holds everything const fits = (capacity) => { let used = 1; let load = 0; for (const w of weights) { if (load + w > capacity) { used++; // start a new day load = 0; } load += w; } return used <= days; }; while (lo < hi) { const mid = lo + Math.floor((hi - lo) / 2); if (fits(mid)) hi = mid; else lo = mid + 1; } return lo; } ``` ## Бинарный поиск по ответу: частые ошибки - **Начинают диапазон с нуля** Нижняя граница должна быть значением, которое в принципе может сработать. Для вместимости это самый большой предмет. - **Проверка не монотонна** Если больший кандидат может провалиться после прошедшего меньшего, делить пополам нельзя. Сначала проверьте направление. - **Теряют последнего сработавшего кандидата** Либо сохраните его в переменной, либо двигайте верх в mid, а не за него. - **Зацикливаются на дробном ответе** На вещественных числах низ никогда не обгонит верх. Сделайте фиксированные сто раундов. ## Бинарный поиск по ответу: задачи с собеседований Паттерн «Бинарный поиск по ответу» решает такие задачи с собеседований: Коко ест бананы, Вместимость для отгрузки за D дней, Разбить массив с минимальной большой суммой, Минимум дней для m букетов, Магнитная сила между шарами, Наименьший делитель при пороге и Минимизировать расстояние между заправками. - **Коко ест бананы** Кандидат это скорость, проверка считает часы. - **Вместимость для отгрузки за D дней** Кандидат это дневная загрузка. - **Разбить массив с минимальной большой суммой** Кандидат это предел суммы одной части. - **Минимум дней для m букетов** Кандидат это день, проверка считает готовые букеты. - **Магнитная сила между шарами** Здесь максимизируют минимальный зазор, а не наоборот. - **Наименьший делитель при пороге** Кандидат это сам делитель. - **Минимизировать расстояние между заправками** Ответ дробный, поэтому цикл делает фиксированное число шагов. ## JavaScript ```javascript function smallestFeasible(lo, hi, canDo) { while (lo < hi) { const mid = lo + ((hi - lo) >> 1); if (canDo(mid)) hi = mid; else lo = mid + 1; } return lo; } // canDo(x) must be monotonic: false...false, true...true ``` ## TypeScript ```typescript function smallestFeasible(lo: number, hi: number, canDo: (x: number) => boolean): number { while (lo < hi) { const mid = lo + ((hi - lo) >> 1); if (canDo(mid)) hi = mid; else lo = mid + 1; } return lo; } // canDo(x) must be monotonic: false...false, true...true ``` ## Python ```python def smallest_feasible(lo, hi, can_do): while lo < hi: mid = lo + (hi - lo) // 2 if can_do(mid): hi = mid else: lo = mid + 1 return lo # can_do(x) must be monotonic: False...False, True...True ``` ## PHP ```php function smallestFeasible(int $lo, int $hi, callable $canDo): int { while ($lo < $hi) { $mid = $lo + intdiv($hi - $lo, 2); if ($canDo($mid)) $hi = $mid; else $lo = $mid + 1; } return $lo; } // canDo($x) must be monotonic: false...false, true...true ``` ## Java ```java public static int smallestFeasible(int lo, int hi, IntPredicate canDo) { while (lo < hi) { int mid = lo + ((hi - lo) >>> 1); if (canDo.test(mid)) hi = mid; else lo = mid + 1; } return lo; } // canDo(x) must be monotonic: false...false, true...true public interface IntPredicate { boolean test(int x); } ``` ## C# ```csharp public static int SmallestFeasible(int lo, int hi, Func canDo) { while (lo < hi) { int mid = lo + (int)(((uint)(hi - lo)) >> 1); if (canDo(mid)) hi = mid; else lo = mid + 1; } return lo; } // canDo(x) must be monotonic: false...false, true...true ``` --- title: "Элементарные сортировки (выбором, пузырьком, вставками)" url: https://algopath.pro/ru/patterns/elementary-sort language: ru summary: "Сортировки выбором, пузырьком и вставками сравнивают соседей и меняют их местами. Все три стоят O(n в квадрате) на перемешанных данных." updated: 2026-10-17 --- # Элементарные сортировки (выбором, пузырьком, вставками) Сортировки выбором, пузырьком и вставками сравнивают соседей и меняют их местами. Все три стоят O(n в квадрате) на перемешанных данных. ## Элементарные сортировки (выбором, пузырьком, вставками): как это работает? Все три держат отсортированную часть и неотсортированную. Каждый раунд переносит через границу один элемент. Сортировка выбором ищет в неотсортированной части минимум. Она меняет его местами с нужной позицией. Пузырёк сравнивает соседние пары и меняет их при нарушении порядка. Наибольшее значение уплывает в конец. Вставки берут следующий элемент и двигают его влево. Движение прекращается, как только сосед меньше. Знать стоит именно вставки. На почти отсортированных данных они почти ничего не делают. Все три работают на месте с O(1) памяти. Вставки и пузырёк устойчивы, выбор нет. Источники: [Википедия: Сортировка вставками](https://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0_%D0%B2%D1%81%D1%82%D0%B0%D0%B2%D0%BA%D0%B0%D0%BC%D0%B8), [Википедия: Сортировка выбором](https://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0_%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%D0%BE%D0%BC) - `[5 | 2, 4, 1]` Сортировка вставками. Отсортирован пока только первый элемент. - `[2, 5 | 4, 1]` Двойка проходит мимо пятёрки. Два элемента упорядочены. - `[2, 4, 5 | 1]` Четвёрка проходит мимо пятёрки и встаёт после двойки. - `[1, 2, 4, 5]` Единица уезжает в самое начало. Это самый дорогой ход здесь. - `3 раунда` Почти отсортированный вход стоит одного сравнения на элемент. Сдвигов не будет вовсе. ## Элементарные сортировки (выбором, пузырьком, вставками): когда применять? Паттерн «Элементарные сортировки (выбором, пузырьком, вставками)» нужен, когда в условии есть слова «n маленькое (десятки или пара сотен)», «массив уже почти отсортирован» или «сортировка на месте с O(1) дополнительной памяти». Решение на нём обычно укладывается в O(n^2). - n маленькое (десятки или пара сотен) - массив уже почти отсортирован - сортировка на месте с O(1) дополнительной памяти - учебный/собеседовательный вопрос о механике сортировки - важна стабильность, а простота приемлема ## Элементарные сортировки (выбором, пузырьком, вставками): с чем путают? Паттерн «Элементарные сортировки (выбором, пузырьком, вставками)» чаще всего путают с этими паттернами: Быстрая сортировка (merge / quick), Сортировка без сравнений (подсчётом / поразрядная), Сортировка с пользовательским компаратором и Бинарная куча / очередь с приоритетом. - **Быстрая сортировка (merge / quick)** - Merge и quick делят массив и стоят n log n. Эти три ничего не делят. - **Сортировка без сравнений (подсчётом / поразрядная)** - Сортировка подсчётом читает значения, а не сравнивает их. Для этого нужен небольшой диапазон ключей. - **Сортировка с пользовательским компаратором** - Та страница про то, какой порядок вам нужен. Эта про то, как порядок получается. - **Бинарная куча / очередь с приоритетом** - Сортировка выбором каждый раунд ищет минимум перебором. Куча отдаёт его за log n. ## Элементарные сортировки (выбором, пузырьком, вставками): сложность по времени и памяти У паттерна «Элементарные сортировки (выбором, пузырьком, вставками)» целевая сложность O(n^2). n примерно до 5000 нормально при O(n в квадрате). На почти отсортированных данных вставки дают O(n). ## Элементарные сортировки (выбором, пузырьком, вставками): разбор примера ### Сортировка вставками на связном списке Дан односвязный список, его надо отсортировать вставками. Перевязывать узлы можно. Копировать значения в массив нельзя. Стройте второй, отсортированный список по одному узлу. Для каждого узла идите по отсортированному списку с начала, пока следующее значение не станет больше. Там и вставляйте. ```javascript function insertionSortList(head) { const dummy = new ListNode(0); let node = head; while (node) { const next = node.next; // walk from the front each time: the sorted part has no back links let prev = dummy; while (prev.next && prev.next.val < node.val) { prev = prev.next; } node.next = prev.next; prev.next = node; node = next; } return dummy.next; } ``` ## Элементарные сортировки (выбором, пузырьком, вставками): частые ошибки - **По привычке берут пузырёк** Пузырёк делает больше всех работы при том же классе сложности. Вставки лучше по умолчанию. - **Сортируют так большой вход** При n в 1e5 квадрат это 1e10 операций. Возьмите встроенную сортировку. - **Ведут внутренний цикл до нулевого индекса** Вставки должны остановиться, когда левый сосед меньше. Дальнейший проход убивает их лучший случай. - **Берут выбор там, где важны равные ключи** Дальний обмен может перекинуть равные ключи друг через друга. Вставки сохраняют их исходный порядок. ## Элементарные сортировки (выбором, пузырьком, вставками): задачи с собеседований Паттерн «Элементарные сортировки (выбором, пузырьком, вставками)» решает такие задачи с собеседований: Отсортировать массив, Сортировка вставками на списке, Сортировка цветов, Почти отсортированный массив, Проверка роста в шеренге, Сортировка по чётности и Первое пропущенное положительное. - **Отсортировать массив** Любой из трёх хватает при нескольких тысячах элементов. - **Сортировка вставками на списке** Тот же цикл, но по узлам, а не по ячейкам. - **Сортировка цветов** Значений всего три, поэтому один проход бьёт любую сортировку сравнением. - **Почти отсортированный массив** Вставки линейны, когда каждый элемент близко к своему месту. - **Проверка роста в шеренге** Сравните ряд с его же отсортированной копией. - **Сортировка по чётности** Это разбиение, а не полная сортировка. - **Первое пропущенное положительное** Циклическая сортировка ставит каждое значение на свой индекс. ## JavaScript ```javascript function insertionSort(arr) { for (let i = 1; i < arr.length; i++) { const key = arr[i]; let j = i - 1; while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } return arr; } ``` ## TypeScript ```typescript function insertionSort(arr: number[]): number[] { for (let i = 1; i < arr.length; i++) { const key = arr[i]; let j = i - 1; while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } return arr; } ``` ## Python ```python def insertion_sort(arr): for i in range(1, len(arr)): key = arr[i] j = i - 1 while j >= 0 and arr[j] > key: arr[j + 1] = arr[j] j -= 1 arr[j + 1] = key return arr ``` ## PHP ```php function insertionSort(array $arr): array { for ($i = 1; $i < count($arr); $i++) { $key = $arr[$i]; $j = $i - 1; while ($j >= 0 && $arr[$j] > $key) { $arr[$j + 1] = $arr[$j]; $j--; } $arr[$j + 1] = $key; } return $arr; } ``` ## Java ```java public static int[] insertionSort(int[] arr) { for (int i = 1; i < arr.length; i++) { int key = arr[i]; int j = i - 1; while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } return arr; } ``` ## C# ```csharp public static int[] InsertionSort(int[] arr) { for (int i = 1; i < arr.Length; i++) { int key = arr[i]; int j = i - 1; while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } return arr; } ``` --- title: "Быстрая сортировка (merge / quick)" url: https://algopath.pro/ru/patterns/fast-sort language: ru summary: "Разрежьте массив надвое, отсортируйте каждую часть и соберите обратно. Merge режет по позиции, а quick режет по значению." updated: 2026-10-17 --- # Быстрая сортировка (merge / quick) Разрежьте массив надвое, отсортируйте каждую часть и соберите обратно. Merge режет по позиции, а quick режет по значению. ## Быстрая сортировка (merge / quick): как это работает? Обе сортировки режут одну задачу на две поменьше. Разница в том, где проходит разрез. Merge режет посередине, не глядя на значения. Каждая половина сортируется тем же способом. Слияние двух отсортированных половин это один линейный проход. Берите тот передний элемент, что меньше. Quick выбирает опорное значение и разбивает массив вокруг него. Меньшие уходят влево, большие вправо. После разбиения опорный элемент стоит на своём окончательном месте. Дальше каждая сторона сортируется отдельно. Обе делают log n уровней работы над n элементами. Плохая опора превращает quick в O(n в квадрате). Источники: [Википедия: Сортировка слиянием](https://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0_%D1%81%D0%BB%D0%B8%D1%8F%D0%BD%D0%B8%D0%B5%D0%BC), [Википедия: Быстрая сортировка](https://ru.wikipedia.org/wiki/%D0%91%D1%8B%D1%81%D1%82%D1%80%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0), [Hoare (1962), Quicksort (англ.)](https://doi.org/10.1093/comjnl/5.1.10) - `[3, 1, 4, 2]` Сортировка слиянием. Режем посередине, не глядя на значения. - `[3, 1] и [4, 2]` Каждую половину режем снова, до одиночных элементов. - `[1, 3] и [2, 4]` Один элемент уже отсортирован. Каждая пара сливается за одно сравнение. - `[1, 2, ...]` Слияние сравнивает два передних элемента. Единица меньше двойки и идёт первой. - `[1, 2, 3, 4]` Последнее слияние это один проход по четырём элементам. Всего два уровня. ## Быстрая сортировка (merge / quick): когда применять? Паттерн «Быстрая сортировка (merge / quick)» нужен, когда в условии есть слова «сортировка массива реального размера (тысячи+)», «нужна гарантированная или ожидаемая O(n log n)» или «сортировка как подготовительный шаг перед другим алгоритмом (два указателя, жадный, сканирующая прямая)». Решение на нём обычно укладывается в O(n log n). - сортировка массива реального размера (тысячи+) - нужна гарантированная или ожидаемая O(n log n) - сортировка как подготовительный шаг перед другим алгоритмом (два указателя, жадный, сканирующая прямая) - нужна стабильность (сортировка слиянием) или сортировка на месте (быстрая сортировка) - просто «отсортируй это» без намёка на малое n или особые ключи ## Быстрая сортировка (merge / quick): с чем путают? Паттерн «Быстрая сортировка (merge / quick)» чаще всего путают с этими паттернами: Элементарные сортировки (выбором, пузырьком, вставками), Сортировка без сравнений (подсчётом / поразрядная), Бинарная куча / очередь с приоритетом и Рекурсия. - **Элементарные сортировки (выбором, пузырьком, вставками)** - Те ничего не делят и стоят n в квадрате. Именно деление и покупает log n. - **Сортировка без сравнений (подсчётом / поразрядная)** - Подсчёт и разряды обгоняют n log n, читая цифры. Им нужны ключи в узком диапазоне. - **Бинарная куча / очередь с приоритетом** - Пирамидальная сортировка тоже n log n и без буфера. На практике она медленнее на реальных данных. - **Рекурсия** - Рекурсия это механизм, а не сам приём. Главное здесь, где проходит разрез. ## Быстрая сортировка (merge / quick): сложность по времени и памяти У паттерна «Быстрая сортировка (merge / quick)» целевая сложность O(n log n). n до 1e6 даёт O(n log n). Merge требует O(n) дополнительной памяти, quick требует O(log n). ## Быстрая сортировка (merge / quick): разбор примера ### K-е по величине значение Верните k-е по величине значение в неотсортированном массиве. Полная сортировка сработает, но сделает гораздо больше, чем спросили. Разбейте массив вокруг опорного значения, как это делает quick sort. Опорный элемент встанет на своё окончательное место. Следующий раунд нужен только той стороне, где лежит позиция k. ```javascript function findKthLargest(nums, k) { const target = nums.length - k; // the slot it would hold once sorted let lo = 0; let hi = nums.length - 1; while (lo < hi) { const pivot = nums[hi]; let split = lo; for (let i = lo; i < hi; i++) { if (nums[i] < pivot) { [nums[i], nums[split]] = [nums[split], nums[i]]; split++; } } [nums[split], nums[hi]] = [nums[hi], nums[split]]; // the pivot is final, and only one side can hold the answer if (split === target) return nums[split]; if (split < target) lo = split + 1; else hi = split - 1; } return nums[lo]; } ``` ## Быстрая сортировка (merge / quick): частые ошибки - **Всегда берут первый элемент опорным** Тогда отсортированный вход делится на один и n минус один. Берите случайный или средний элемент. - **При равенстве берут элемент справа** Взятие правого при равенстве переставляет равные ключи. При совпадении берите левую половину. - **Спускаются в обе стороны при quickselect** Нужную позицию может содержать только одна сторона. Обход обеих стоит полной сортировки. - **Забывают про буфер сортировки слиянием** Сортировка слиянием выделяет второй массив размером со вход. На очень больших массивах это заметно. ## Быстрая сортировка (merge / quick): задачи с собеседований Паттерн «Быстрая сортировка (merge / quick)» решает такие задачи с собеседований: Отсортировать массив, K-й по величине элемент, Слияние отсортированных массивов, Сколько меньших справа, Обратные пары, Сортировка связного списка и Волнообразная сортировка II. - **Отсортировать массив** Чистая форма, обычно это сортировка слиянием. - **K-й по величине элемент** Одно разбиение за раунд вместо полной сортировки. - **Слияние отсортированных массивов** Половина слияния из merge sort сама по себе. - **Сколько меньших справа** Подсчёт идёт прямо во время слияния. - **Обратные пары** Тот же подсчёт, но с другим сравнением. - **Сортировка связного списка** Сортировка слиянием по узлам, без буфера. - **Волнообразная сортировка II** Quickselect находит медиану, вокруг неё расставляются значения. ## JavaScript ```javascript function mergeSort(arr) { if (arr.length <= 1) return arr; const mid = arr.length >> 1; const left = mergeSort(arr.slice(0, mid)); const right = mergeSort(arr.slice(mid)); const out = []; let i = 0, j = 0; while (i < left.length && j < right.length) { out.push(left[i] <= right[j] ? left[i++] : right[j++]); } return out.concat(left.slice(i), right.slice(j)); } ``` ## TypeScript ```typescript function mergeSort(arr: number[]): number[] { if (arr.length <= 1) return arr; const mid = arr.length >> 1; const left = mergeSort(arr.slice(0, mid)); const right = mergeSort(arr.slice(mid)); const out: number[] = []; let i = 0, j = 0; while (i < left.length && j < right.length) { out.push(left[i] <= right[j] ? left[i++] : right[j++]); } return out.concat(left.slice(i), right.slice(j)); } ``` ## Python ```python def merge_sort(arr): if len(arr) <= 1: return arr mid = len(arr) // 2 left, right = merge_sort(arr[:mid]), merge_sort(arr[mid:]) out, i, j = [], 0, 0 while i < len(left) and j < len(right): if left[i] <= right[j]: out.append(left[i]); i += 1 else: out.append(right[j]); j += 1 return out + left[i:] + right[j:] ``` ## PHP ```php function mergeSort(array $arr): array { if (count($arr) <= 1) return $arr; $mid = intdiv(count($arr), 2); $left = mergeSort(array_slice($arr, 0, $mid)); $right = mergeSort(array_slice($arr, $mid)); $out = []; $i = 0; $j = 0; while ($i < count($left) && $j < count($right)) { $out[] = $left[$i] <= $right[$j] ? $left[$i++] : $right[$j++]; } return array_merge($out, array_slice($left, $i), array_slice($right, $j)); } ``` ## Java ```java public static int[] mergeSort(int[] arr) { if (arr.length <= 1) return arr; int mid = arr.length >>> 1; int[] left = mergeSort(Arrays.copyOfRange(arr, 0, mid)); int[] right = mergeSort(Arrays.copyOfRange(arr, mid, arr.length)); int[] out = new int[arr.length]; int i = 0, j = 0, k = 0; while (i < left.length && j < right.length) { out[k++] = left[i] <= right[j] ? left[i++] : right[j++]; } while (i < left.length) out[k++] = left[i++]; while (j < right.length) out[k++] = right[j++]; return out; } ``` ## C# ```csharp public static int[] MergeSort(int[] arr) { if (arr.Length <= 1) return arr; int mid = arr.Length >> 1; int[] left = MergeSort(arr[..mid]); int[] right = MergeSort(arr[mid..]); var outArr = new int[arr.Length]; int i = 0, j = 0, k = 0; while (i < left.Length && j < right.Length) { outArr[k++] = left[i] <= right[j] ? left[i++] : right[j++]; } while (i < left.Length) outArr[k++] = left[i++]; while (j < right.Length) outArr[k++] = right[j++]; return outArr; } ``` --- title: "Сортировка без сравнений (подсчётом / поразрядная)" url: https://algopath.pro/ru/patterns/non-comparison-sort language: ru summary: "Сортировки подсчётом и поразрядная не сравнивают значения между собой. Они читают сам ключ и обгоняют n log n на узком диапазоне." updated: 2026-10-17 --- # Сортировка без сравнений (подсчётом / поразрядная) Сортировки подсчётом и поразрядная не сравнивают значения между собой. Они читают сам ключ и обгоняют n log n на узком диапазоне. ## Сортировка без сравнений (подсчётом / поразрядная): как это работает? Сортировке подсчётом нужны ключи из известного узкого диапазона. Заведите по счётчику на каждый возможный ключ. Пройдите вход один раз и посчитайте каждый ключ. Теперь известно, сколько каких значений. Превратите счётчики в стартовые позиции. Накопленная сумма говорит, где начинается блок каждого ключа. Пройдите вход второй раз и положите каждый элемент на его позицию. Ничего ни разу не сравнивалось. Поразрядная сортировка применяет ту же идею по одному разряду. Она начинает с младшего разряда. Каждый проход по разряду обязан быть устойчивым. Неустойчивый проход отменяет все предыдущие. Источники: [Википедия: Сортировка подсчётом](https://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0_%D0%BF%D0%BE%D0%B4%D1%81%D1%87%D1%91%D1%82%D0%BE%D0%BC), [Википедия: Поразрядная сортировка](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D1%80%D0%B0%D0%B7%D1%80%D1%8F%D0%B4%D0%BD%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0) - `счётчики = [1, 1, 2]` Сортируем [2, 0, 2, 1]. Один ноль, одна единица, две двойки. - `старты = [0, 1, 2]` Накопленные суммы дают каждому ключу его первую ячейку. - `[0, _, _, _]` Ноль ложится в ячейку 0. Его старт сдвигается на единицу. - `[0, 1, 2, _]` Единица занимает ячейку 1, первая двойка ячейку 2. - `[0, 1, 2, 2]` Вторая двойка занимает ячейку 3. Ни одного сравнения не было. ## Сортировка без сравнений (подсчётом / поразрядная): когда применять? Паттерн «Сортировка без сравнений (подсчётом / поразрядная)» нужен, когда в условии есть слова «ключи это целые числа в небольшом известном диапазоне», «сортировка возрастов, оценок или ограниченных счётчиков» или «нужна O(n), а сравнения стали узким местом». Решение на нём обычно укладывается в O(n). - ключи это целые числа в небольшом известном диапазоне - сортировка возрастов, оценок или ограниченных счётчиков - нужна O(n), а сравнения стали узким местом - сортировка по разрядам или ключу фиксированной ширины - n большое, но диапазон значений намного меньше n ## Сортировка без сравнений (подсчётом / поразрядная): с чем путают? Паттерн «Сортировка без сравнений (подсчётом / поразрядная)» чаще всего путают с этими паттернами: Быстрая сортировка (merge / quick), Хеш-множество / словарь, Сортировка с пользовательским компаратором и Элементарные сортировки (выбором, пузырьком, вставками). - **Быстрая сортировка (merge / quick)** - Никакая сортировка сравнением не быстрее n log n. Чтение ключа обходит этот предел целиком. - **Хеш-множество / словарь** - Словарь тоже считает значения, но порядка не хранит. Сортировка подсчётом делает значение индексом. - **Сортировка с пользовательским компаратором** - Компаратор описывает порядок между двумя элементами. Подсчёт вообще не смотрит на пары. - **Элементарные сортировки (выбором, пузырьком, вставками)** - Те сравнивают соседей и стоят n в квадрате. Подсчёт линеен, если диапазон позволяет. ## Сортировка без сравнений (подсчётом / поразрядная): сложность по времени и памяти У паттерна «Сортировка без сравнений (подсчётом / поразрядная)» целевая сложность O(n). n значений с ключами меньше k дают O(n + k). Поразрядная стоит O(d умножить на n) при d разрядах. ## Сортировка без сравнений (подсчётом / поразрядная): разбор примера ### K самых частых значений Верните k значений, которые встречаются чаще всего. Сортировка счётчиков стоит n log n, а полный порядок задаче не нужен. Посчитайте все значения словарём. Счётчик не может превысить длину массива. Используйте сам счётчик как индекс в списке корзин. Читайте корзины с конца. ```javascript function topKFrequent(nums, k) { const count = new Map(); for (const x of nums) count.set(x, (count.get(x) ?? 0) + 1); // bucket i holds every value that appeared exactly i times const buckets = Array.from({ length: nums.length + 1 }, () => []); for (const [value, c] of count) buckets[c].push(value); const answer = []; for (let c = buckets.length - 1; c >= 1 && answer.length < k; c--) { for (const value of buckets[c]) { answer.push(value); if (answer.length === k) break; } } return answer; } ``` ## Сортировка без сравнений (подсчётом / поразрядная): частые ошибки - **Берут её на огромном диапазоне ключей** Счётчик на каждый ключ означает память размером с диапазон. Значения до 1e9 делают это невозможным. - **Неустойчивый проход внутри поразрядной** Каждый проход по разряду обязан сохранить прошлый порядок. Иначе ранняя работа пропадает. - **Забывают про отрицательные значения** Индекс массива не бывает отрицательным. Сдвиньте все ключи на минимум. - **Считают дробные числа или строки** Ключ обязан быть ограниченным целым. Всё остальное требует сортировки сравнением. ## Сортировка без сравнений (подсчётом / поразрядная): задачи с собеседований Паттерн «Сортировка без сравнений (подсчётом / поразрядная)» решает такие задачи с собеседований: Сортировка небольших целых, Сортировка цветов, K самых частых элементов, Индекс Хирша, Максимальный разрыв, Относительная сортировка массива и Сортировка символов по частоте. - **Сортировка небольших целых** По счётчику на значение, потом чтение по порядку. - **Сортировка цветов** Возможных ключей три, значит и счётчика три. - **K самых частых элементов** Сам счётчик становится индексом корзины. - **Индекс Хирша** Счётчики цитирований, ограниченные числом работ. - **Максимальный разрыв** Поразрядная сортировка и делает линейное решение возможным. - **Относительная сортировка массива** Подсчёт с порядком, заданным другим списком. - **Сортировка символов по частоте** Корзины по числу вхождений буквы. ## JavaScript ```javascript function countingSort(arr, maxVal) { const counts = new Array(maxVal + 1).fill(0); for (const x of arr) counts[x]++; const out = []; for (let v = 0; v <= maxVal; v++) { while (counts[v]-- > 0) out.push(v); } return out; } ``` ## TypeScript ```typescript function countingSort(arr: number[], maxVal: number): number[] { const counts: number[] = new Array(maxVal + 1).fill(0); for (const x of arr) counts[x]++; const out: number[] = []; for (let v = 0; v <= maxVal; v++) { while (counts[v]-- > 0) out.push(v); } return out; } ``` ## Python ```python def counting_sort(arr, max_val): counts = [0] * (max_val + 1) for x in arr: counts[x] += 1 out = [] for v, c in enumerate(counts): out.extend([v] * c) return out ``` ## PHP ```php function countingSort(array $arr, int $maxVal): array { $counts = array_fill(0, $maxVal + 1, 0); foreach ($arr as $x) $counts[$x]++; $out = []; for ($v = 0; $v <= $maxVal; $v++) { for ($c = $counts[$v]; $c > 0; $c--) $out[] = $v; } return $out; } ``` ## Java ```java public static int[] countingSort(int[] arr, int maxVal) { int[] counts = new int[maxVal + 1]; for (int x : arr) counts[x]++; int[] out = new int[arr.length]; int k = 0; for (int v = 0; v <= maxVal; v++) { while (counts[v]-- > 0) out[k++] = v; } return out; } ``` ## C# ```csharp public static int[] CountingSort(int[] arr, int maxVal) { var counts = new int[maxVal + 1]; foreach (int x in arr) counts[x]++; var outArr = new int[arr.Length]; int k = 0; for (int v = 0; v <= maxVal; v++) { while (counts[v]-- > 0) outArr[k++] = v; } return outArr; } ``` --- title: "Сортировка с пользовательским компаратором" url: https://algopath.pro/ru/patterns/sort-comparators language: ru summary: "Компаратор отвечает ровно на один вопрос: какой из двух элементов идёт раньше. Всё остальное берёт на себя сама сортировка." updated: 2026-10-17 --- # Сортировка с пользовательским компаратором Компаратор отвечает ровно на один вопрос: какой из двух элементов идёт раньше. Всё остальное берёт на себя сама сортировка. ## Сортировка с пользовательским компаратором: как это работает? Компаратор берёт два элемента и возвращает число. Отрицательное значит, что первый идёт раньше. Ноль значит, что элементы равны. Устойчивая сортировка оставит их в исходном порядке. Для обычных чисел возвращайте a минус b по возрастанию. Избегайте вычитания, если значения могут переполниться. При нескольких ключах сравните первый. К следующему переходите только при равенстве. Правило обязано быть согласованным. Если a раньше b, а b раньше c, то a раньше c. Компаратор вызывается n log n раз. Всё дорогое внутри него посчитайте один раз заранее. Источники: [Документация Python (англ.): Sorting techniques](https://docs.python.org/3/howto/sorting.html), [MDN (англ.): Array.prototype.sort()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) - `[Аня 30, Боря 25, Слава 30]` Сортируем сначала по возрасту, потом по имени. - `сравнение(Аня, Боря)` 30 против 25 даёт положительное число. Боря идёт раньше. - `сравнение(Аня, Слава)` Возраст совпал на 30. Правило переходит к имени. - `Аня раньше Славы` Решает второй ключ. Аня выигрывает по имени. - `[Боря 25, Аня 30, Слава 30]` Два ключа, один проход. Порядок теперь полный. ## Сортировка с пользовательским компаратором: когда применять? Паттерн «Сортировка с пользовательским компаратором» нужен, когда в условии есть слова «сортировка по нескольким полям / разрешение ничьей по второму ключу», «произвольный порядок (не простое возрастание)» или «сортировка по убыванию или по вычисляемому ключу». Решение на нём обычно укладывается в O(n log n). - сортировка по нескольким полям / разрешение ничьей по второму ключу - произвольный порядок (не простое возрастание) - сортировка по убыванию или по вычисляемому ключу - «отсортируй так, чтобы X шло перед Y, если...» - переупорядочивание объектов/записей, а не чисел ## Сортировка с пользовательским компаратором: с чем путают? Паттерн «Сортировка с пользовательским компаратором» чаще всего путают с этими паттернами: Быстрая сортировка (merge / quick), Жадный алгоритм (обменный аргумент), Интервалы: слияние и вставка и Бинарная куча / очередь с приоритетом. - **Быстрая сортировка (merge / quick)** - Та страница про то, как происходит сортировка. Эта про порядок, который вы заказываете. - **Жадный алгоритм (обменный аргумент)** - Жадное рассуждение обычно и выбирает верный порядок. Компаратор это способ его записать. - **Интервалы: слияние и вставка** - Работа с интервалами начинается с сортировки по началу. Слияние это отдельный шаг после неё. - **Бинарная куча / очередь с приоритетом** - Куча берёт тот же компаратор, но держит только начало. Она нужна, когда данные ещё поступают. ## Сортировка с пользовательским компаратором: сложность по времени и памяти У паттерна «Сортировка с пользовательским компаратором» целевая сложность O(n log n). n до 1e6 даёт O(n log n) сравнений. Медленный компаратор умножает это, поэтому ключ считайте заранее. ## Сортировка с пользовательским компаратором: разбор примера ### Собрать из чисел наибольшее Дан список неотрицательных целых. Расставьте их так, чтобы склеенные цифры читались как наибольшее число. Верните строку, потому что результат бывает огромным. Сортировка по значению здесь неверна: 9 обязана идти раньше 30. Сравнивайте два числа по двум склейкам, которые из них получаются. Ставьте a первым, если a плюс b больше. ```javascript function largestNumber(nums) { const parts = nums.map(String); // whichever joined order reads larger wins the comparison parts.sort((a, b) => (b + a).localeCompare(a + b)); if (parts[0] === "0") return "0"; // every value was zero return parts.join(""); } ``` ## Сортировка с пользовательским компаратором: частые ошибки - **Сортируют числа без компаратора** JavaScript по умолчанию сравнивает как строки, поэтому 10 встаёт перед 9. Всегда передавайте компаратор. - **Вычитают очень большие значения** Разность может переполниться или потерять точность. Возвращайте минус один, ноль или один. - **Пишут правило, противоречащее себе** Несогласованный компаратор даёт неопределённый результат. Некоторые движки просто бросают ошибку. - **Считают дорогой ключ внутри него** Компаратор вызывается на каждое сравнение, а не раз на элемент. Посчитайте ключ заранее. ## Сортировка с пользовательским компаратором: задачи с собеседований Паттерн «Сортировка с пользовательским компаратором» решает такие задачи с собеседований: Наибольшее число, Слияние интервалов, Переговорные комнаты, Сортировка по возрастанию частоты, Сортировка по заданному алфавиту, Относительная сортировка массива и K ближайших точек к началу. - **Наибольшее число** Порядок по тому, какая из двух склеек больше. - **Слияние интервалов** Сортировка по началу идёт до всего остального. - **Переговорные комнаты** Та же сортировка, потом проверка каждой соседней пары. - **Сортировка по возрастанию частоты** Первый ключ это частота, второй само значение. - **Сортировка по заданному алфавиту** Порядок задан совсем другой строкой. - **Относительная сортировка массива** Сначала перечисленные значения, остальные по возрастанию. - **K ближайших точек к началу** Сортируйте по квадрату расстояния, а не по корню. ## JavaScript ```javascript // sort by a custom / multi-key order items.sort((a, b) => { if (a.priority !== b.priority) return a.priority - b.priority; return a.name.localeCompare(b.name); }); ``` ## TypeScript ```typescript interface Item { priority: number; name: string; } // sort by a custom / multi-key order function sortItems(items: Item[]): void { items.sort((a, b) => { if (a.priority !== b.priority) return a.priority - b.priority; return a.name.localeCompare(b.name); }); } ``` ## Python ```python from functools import cmp_to_key def compare(a, b): if a["priority"] != b["priority"]: return a["priority"] - b["priority"] return -1 if a["name"] < b["name"] else 1 items.sort(key=cmp_to_key(compare)) # or simply: items.sort(key=lambda x: (x["priority"], x["name"])) ``` ## PHP ```php usort($items, function ($a, $b) { if ($a["priority"] !== $b["priority"]) { return $a["priority"] <=> $b["priority"]; } return strcmp($a["name"], $b["name"]); }); ``` ## Java ```java // sort by a custom / multi-key order public static void sortItems(List items) { items.sort(Comparator .comparingInt((Item a) -> a.priority) .thenComparing(a -> a.name)); } public static class Item { public int priority; public String name; } ``` ## C# ```csharp // sort by a custom / multi-key order public static void SortItems(List items) { items.Sort((a, b) => a.Priority != b.Priority ? a.Priority.CompareTo(b.Priority) : string.CompareOrdinal(a.Name, b.Name)); } public class Item { public int Priority; public string Name = ""; } ``` --- title: "Интервалы: слияние и вставка" url: https://algopath.pro/ru/patterns/intervals-merge language: ru summary: "Отсортируйте интервалы по началу и пройдите их один раз. Соседи сливаются, если следующее начало не позже текущего конца." updated: 2026-10-15 --- # Интервалы: слияние и вставка Отсортируйте интервалы по началу и пройдите их один раз. Соседи сливаются, если следующее начало не позже текущего конца. ## Интервалы: слияние и вставка: как это работает? Отсортируйте интервалы по значению начала. Без этого порядка дальше ничего не работает. Возьмите первый интервал как текущий блок. Все следующие сравниваются с ним. Посмотрите на следующий интервал и его начало. Сравните это начало с текущим концом. Если начало не позже текущего конца, интервалы соприкасаются. Расширьте конец до большего из двух. Если начало позже, блок закончен. Запишите его и сделайте новый интервал текущим. Один проход покрывает весь список. Последний блок записывается после цикла. - `текущий = [1, 3]` Список отсортирован по началу. Первый интервал открывает блок. - `следующий = [2, 6]` 2 не позже 3, значит есть пересечение. Конец растёт до 6. - `текущий = [1, 6], следующий = [8, 10]` 8 позже 6. Блок закончен и записан. - `текущий = [8, 10], следующий = [9, 12]` 9 не позже 10. Конец растёт до 12. - `[[1, 6], [8, 12]]` Последний блок записан после цикла. Четыре интервала стали двумя. ## Интервалы: слияние и вставка: когда применять? Паттерн «Интервалы: слияние и вставка» нужен, когда в условии есть слова «слить перекрывающиеся интервалы», «вставить новый интервал в отсортированный список» или «встречи, брони или диапазоны, которые пересекаются». Решение на нём обычно укладывается в O(n log n). - слить перекрывающиеся интервалы - вставить новый интервал в отсортированный список - встречи, брони или диапазоны, которые пересекаются - заданы как пары [начало, конец] - свободное/занятое время между интервалами ## Интервалы: слияние и вставка: с чем путают? Паттерн «Интервалы: слияние и вставка» чаще всего путают с этими паттернами: Линия развёртки (подсчёт событий), Жадный алгоритм (обменный аргумент), Сортировка с пользовательским компаратором и Массив разностей. - **Линия развёртки (подсчёт событий)** - Развёртка разбивает интервал на два события и считает их. Слияние держит интервалы целыми. - **Жадный алгоритм (обменный аргумент)** - Убрать поменьше интервалов это жадный выбор по концу. Слияние просто соединяет пересечения. - **Сортировка с пользовательским компаратором** - Сортировка по началу это подготовка. Эта страница про проход, который идёт следом. - **Массив разностей** - Он считает глубину перекрытия в каждой точке. Слияние возвращает интервалы, а не числа. ## Интервалы: слияние и вставка: сложность по времени и памяти У паттерна «Интервалы: слияние и вставка» целевая сложность O(n log n). n до 1e6 стоит O(n log n), и всё это сортировка. Проход после неё стоит O(n). ## Интервалы: слияние и вставка: разбор примера ### Вставка интервала в отсортированный список Дан отсортированный список непересекающихся интервалов и один новый интервал. Вставьте его, слейте всё, чего он касается, и сохраните порядок без пересечений. Перенесите все интервалы, которые кончаются до начала нового. Потом поглотите все пересекающиеся, расширяя новый интервал. Оставшийся хвост перенесите как есть. ```javascript function insert(intervals, newInterval) { const result = []; let [start, end] = newInterval; let i = 0; while (i < intervals.length && intervals[i][1] < start) { result.push(intervals[i]); // ends before the new one begins i++; } while (i < intervals.length && intervals[i][0] <= end) { start = Math.min(start, intervals[i][0]); end = Math.max(end, intervals[i][1]); // the later interval may end sooner i++; } result.push([start, end]); while (i < intervals.length) { result.push(intervals[i]); i++; } return result; } ``` ## Интервалы: слияние и вставка: частые ошибки - **Пропускают сортировку** Один проход рассчитывает, что начала только растут. На неотсортированном входе сливаются не те пары. - **Угадывают знак сравнения** Соприкасаются ли [1, 2] и [2, 3], решает условие задачи. Прочитайте его до выбора оператора. - **Забывают последний блок** Текущий интервал записывается только после цикла. Без этого в ответе не хватает одного. - **Берут конец у более позднего интервала** Более поздний интервал может закончиться раньше. Новый конец это максимум из двух. ## Интервалы: слияние и вставка: задачи с собеседований Паттерн «Интервалы: слияние и вставка» решает такие задачи с собеседований: Слияние интервалов, Вставка интервала, Непересекающиеся интервалы, Переговорные комнаты, Пересечение двух списков интервалов, Свободное время сотрудников и Удаление вложенных интервалов. - **Слияние интервалов** Чистая форма: сортировка по началу и один проход. - **Вставка интервала** Список уже отсортирован, сливать нужно только середину. - **Непересекающиеся интервалы** Жадно оставляйте тот, что кончается раньше. - **Переговорные комнаты** Любое пересечение делает ответ отрицательным. - **Пересечение двух списков интервалов** Два отсортированных списка обходятся двумя указателями. - **Свободное время сотрудников** Слейте все занятые блоки и прочитайте промежутки. - **Удаление вложенных интервалов** Сортируйте по началу, а при равенстве длинный вперёд. ## JavaScript ```javascript function mergeIntervals(intervals) { intervals.sort((a, b) => a[0] - b[0]); const result = []; for (const [start, end] of intervals) { const last = result[result.length - 1]; if (last && start <= last[1]) { last[1] = Math.max(last[1], end); } else { result.push([start, end]); } } return result; } ``` ## TypeScript ```typescript function mergeIntervals(intervals: number[][]): number[][] { intervals.sort((a, b) => a[0] - b[0]); const result: number[][] = []; for (const [start, end] of intervals) { const last = result[result.length - 1]; if (last && start <= last[1]) { last[1] = Math.max(last[1], end); } else { result.push([start, end]); } } return result; } ``` ## Python ```python def merge_intervals(intervals): intervals.sort(key=lambda x: x[0]) result = [] for start, end in intervals: if result and start <= result[-1][1]: result[-1][1] = max(result[-1][1], end) else: result.append([start, end]) return result ``` ## PHP ```php function mergeIntervals(array $intervals): array { usort($intervals, fn($a, $b) => $a[0] <=> $b[0]); $result = []; foreach ($intervals as [$start, $end]) { $lastIdx = count($result) - 1; if ($lastIdx >= 0 && $start <= $result[$lastIdx][1]) { $result[$lastIdx][1] = max($result[$lastIdx][1], $end); } else { $result[] = [$start, $end]; } } return $result; } ``` ## Java ```java public static List mergeIntervals(int[][] intervals) { Arrays.sort(intervals, Comparator.comparingInt(a -> a[0])); List result = new ArrayList<>(); for (int[] interval : intervals) { int[] last = result.isEmpty() ? null : result.get(result.size() - 1); if (last != null && interval[0] <= last[1]) { last[1] = Math.max(last[1], interval[1]); } else { result.add(new int[] { interval[0], interval[1] }); } } return result; } ``` ## C# ```csharp public static List MergeIntervals(int[][] intervals) { Array.Sort(intervals, (a, b) => a[0].CompareTo(b[0])); var result = new List(); foreach (var interval in intervals) { var last = result.Count > 0 ? result[^1] : null; if (last != null && interval[0] <= last[1]) { last[1] = Math.Max(last[1], interval[1]); } else { result.Add(new int[] { interval[0], interval[1] }); } } return result; } ``` --- title: "Линия развёртки (подсчёт событий)" url: https://algopath.pro/ru/patterns/sweep-line language: ru summary: "Превратите каждый интервал в событие начала и событие конца. Отсортируйте события по координате и пройдите со счётчиком." updated: 2026-10-17 --- # Линия развёртки (подсчёт событий) Превратите каждый интервал в событие начала и событие конца. Отсортируйте события по координате и пройдите со счётчиком. ## Линия развёртки (подсчёт событий): как это работает? Каждый интервал превращается в два события. Начало на левой координате, конец на правой. Сложите все события в один список. Начало несёт плюс один, конец минус один. Отсортируйте список по координате. Для совпадений нужно правило, и оно зависит от задачи. Идите по отсортированным событиям и ведите счётчик. Он всегда хранит число открытых интервалов. На каждом шаге записывайте то, что просит задача. Обычно это максимум, которого достиг счётчик. Координатам не нужно быть плотными. Важен только их взаимный порядок. Источники: [Wikipedia (англ.): Sweep line algorithm](https://en.wikipedia.org/wiki/Sweep_line_algorithm) - `встречи [1, 4], [2, 5], [7, 9]` Три встречи превращаются в шесть отдельных событий. - `1+, 2+, 4-, 5-, 7+, 9-` События отсортированы по времени. Интервалов больше нет. - `счётчик = 1 в момент 1` Открывается первая встреча. Занята одна комната. - `счётчик = 2 в момент 2` Вторая открылась до конца первой. Нужны две комнаты. - `счётчик = 0 в момент 5` Обе закрылись до момента 7. Пик равнялся двум. ## Линия развёртки (подсчёт событий): когда применять? Паттерн «Линия развёртки (подсчёт событий)» нужен, когда в условии есть слова «максимум одновременно пересекающихся встреч/интервалов», «минимум комнат/ресурсов» или «самый загруженный момент времени». Решение на нём обычно укладывается в O(n log n). - максимум одновременно пересекающихся встреч/интервалов - минимум комнат/ресурсов - самый загруженный момент времени - сколько интервалов покрывают заданную точку - события, происходящие в одно и то же время ## Линия развёртки (подсчёт событий): с чем путают? Паттерн «Линия развёртки (подсчёт событий)» чаще всего путают с этими паттернами: Интервалы: слияние и вставка, Массив разностей, Бинарная куча / очередь с приоритетом и Префиксные суммы. - **Интервалы: слияние и вставка** - Слияние держит интервалы целыми и соединяет соприкасающиеся. Развёртка про них забывает и считает. - **Массив разностей** - Это развёртка, где координаты служат индексами массива. Ей нужен узкий плотный диапазон. - **Бинарная куча / очередь с приоритетом** - Куча держит открытые интервалы и знает, какой кончится раньше. Счётчик знает только сколько их. - **Префиксные суммы** - Префиксные суммы отвечают по фиксированным позициям массива. Развёртка идёт по событиям в порядке сортировки. ## Линия развёртки (подсчёт событий): сложность по времени и памяти У паттерна «Линия развёртки (подсчёт событий)» целевая сложность O(n log n). n интервалов дают 2n событий, значит O(n log n) на сортировку. Сам проход стоит O(n). ## Линия развёртки (подсчёт событий): разбор примера ### Сколько нужно переговорных комнат Даны время начала и конца каждой встречи. Найдите наименьшее число комнат, вмещающее все встречи. Двум встречам нужны разные комнаты, если они пересекаются. Отсортируйте начала и концы в два отдельных списка. Идите по ним вместе: начало занимает комнату, конец возвращает. Ответ это пик счётчика. ```javascript function minMeetingRooms(intervals) { const starts = intervals.map((i) => i[0]).sort((a, b) => a - b); const ends = intervals.map((i) => i[1]).sort((a, b) => a - b); let rooms = 0; let best = 0; let e = 0; for (const start of starts) { // every meeting already finished hands its room back first while (ends[e] <= start) { rooms--; e++; } rooms++; best = Math.max(best, rooms); } return best; } ``` ## Линия развёртки (подсчёт событий): частые ошибки - **Нет правила для событий в одной точке** Освобождает ли конец в 5 комнату для начала в 5, меняет ответ. Решите и закодируйте это. - **Сортируют интервалы вместо событий** Развёртке нужны начала и концы вперемешку. Сортировка целых интервалов держит пары склеенными. - **Читают счётчик после прохода** Ответом обычно служит пик, а не итоговое значение. Итоговое всегда равно нулю, ведь у каждого начала есть свой конец. - **Индексируют массив координатой** Метки времени до 1e9 не бывают индексами массива. Сортируйте события вместо выделения памяти. ## Линия развёртки (подсчёт событий): задачи с собеседований Паттерн «Линия развёртки (подсчёт событий)» решает такие задачи с собеседований: Переговорные комнаты II, Совместные поездки, Мой календарь III, Сколько цветов цветёт, Задача о силуэте города, Свободное время сотрудников и Год максимального населения. - **Переговорные комнаты II** Пик счётчика и есть число комнат. - **Совместные поездки** Пассажиры садятся и выходят на одном маршруте. - **Мой календарь III** Счётчик не должен переходить лимит броней. - **Сколько цветов цветёт** Считайте, сколько диапазонов накрывают каждый спрошенный день. - **Задача о силуэте города** Развёртка с кучей, хранящей текущие высоты. - **Свободное время сотрудников** Все промежутки, где счётчик равен нулю. - **Год максимального населения** Рождения и смерти как события на временной оси. ## JavaScript ```javascript function maxOverlap(intervals) { const events = []; for (const [start, end] of intervals) { events.push([start, 1]); events.push([end, -1]); } events.sort((a, b) => a[0] - b[0] || a[1] - b[1]); let active = 0; let best = 0; for (const [, delta] of events) { active += delta; best = Math.max(best, active); } return best; } ``` ## TypeScript ```typescript function maxOverlap(intervals: number[][]): number { const events: number[][] = []; for (const [start, end] of intervals) { events.push([start, 1]); events.push([end, -1]); } events.sort((a, b) => a[0] - b[0] || a[1] - b[1]); let active = 0; let best = 0; for (const [, delta] of events) { active += delta; best = Math.max(best, active); } return best; } ``` ## Python ```python def max_overlap(intervals): events = [] for start, end in intervals: events.append((start, 1)) events.append((end, -1)) events.sort() active = 0 best = 0 for _, delta in events: active += delta best = max(best, active) return best ``` ## PHP ```php function maxOverlap(array $intervals): int { $events = []; foreach ($intervals as [$start, $end]) { $events[] = [$start, 1]; $events[] = [$end, -1]; } usort($events, fn($a, $b) => $a[0] <=> $b[0] ?: $a[1] <=> $b[1]); $active = 0; $best = 0; foreach ($events as [, $delta]) { $active += $delta; $best = max($best, $active); } return $best; } ``` ## Java ```java public static int maxOverlap(int[][] intervals) { List events = new ArrayList<>(); for (int[] interval : intervals) { events.add(new int[] { interval[0], 1 }); events.add(new int[] { interval[1], -1 }); } events.sort(Comparator .comparingInt((int[] e) -> e[0]) .thenComparingInt(e -> e[1])); int active = 0; int best = 0; for (int[] event : events) { active += event[1]; best = Math.max(best, active); } return best; } ``` ## C# ```csharp public static int MaxOverlap(int[][] intervals) { var events = new List(); foreach (var interval in intervals) { events.Add(new int[] { interval[0], 1 }); events.Add(new int[] { interval[1], -1 }); } events.Sort((a, b) => a[0] != b[0] ? a[0].CompareTo(b[0]) : a[1].CompareTo(b[1])); int active = 0; int best = 0; foreach (var e in events) { active += e[1]; best = Math.Max(best, active); } return best; } ``` --- title: "Разворот связного списка" url: https://algopath.pro/ru/patterns/linked-list-reversal language: ru summary: "Пройдите список один раз и в каждом узле разверните ссылку на предыдущий узел. Трёх локальных переменных для этого хватит." updated: 2026-10-17 --- # Разворот связного списка Пройдите список один раз и в каждом узле разверните ссылку на предыдущий узел. Трёх локальных переменных для этого хватит. ## Разворот связного списка: как это работает? Держите три ссылки: предыдущий узел, текущий и следующий за текущим. Предыдущий начинается с null. Сохраните current.next до того, как что-то менять. Иначе остаток списка потеряется. Направьте current.next на предыдущий узел. Эта одна строка и есть разворот. Сдвиньте предыдущий на текущий, а текущий на сохранённый узел. Окно продвинулось на один шаг. Остановитесь, когда текущий стал null. Предыдущий теперь указывает на бывший последний узел. Верните предыдущий, а не текущий. Бывшая голова стала хвостом. Источники: [Википедия: Связный список](https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9_%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA) - `prev = null, cur = 1` Список читается как 1, 2, 3. Пока ничего не развёрнуто. - `1 указывает на null, cur = 2` Узел 1 теперь смотрит в null. Он стал хвостом. - `2 указывает на 1, cur = 3` Узел 2 смотрит назад на узел 1. Два узла готовы. - `3 указывает на 2, cur = null` Последний узел развернулся. Текущий ушёл за конец. - `голова = 3` В prev лежит новая голова. Три разворота за один проход. ## Разворот связного списка: когда применять? Паттерн «Разворот связного списка» нужен, когда в условии есть слова «развернуть связный список (весь список или между позициями)», «развернуть группами по k» или «без дополнительного массива или с O(1) доп. памяти». Решение на нём обычно укладывается в O(n). - развернуть связный список (весь список или между позициями) - развернуть группами по k - без дополнительного массива или с O(1) доп. памяти - поменять местами пары узлов - односвязный список, доступен только .next ## Разворот связного списка: с чем путают? Паттерн «Разворот связного списка» чаще всего путают с этими паттернами: Слияние и перестройка связного списка, Быстрый и медленный указатели, Стек (LIFO) и Рекурсия. - **Слияние и перестройка связного списка** - Слияние сплетает два списка в один. Разворот перевязывает один список на месте. - **Быстрый и медленный указатели** - Та пара находит середину или цикл обходом. Она не меняет ни одной ссылки. - **Стек (LIFO)** - Сложить все узлы в стек и снять их тоже даёт разворот. Это стоит O(n) памяти. - **Рекурсия** - Рекурсивный вариант читается хорошо, но тратит n кадров стека. Цикл обходится тремя переменными. ## Разворот связного списка: сложность по времени и памяти У паттерна «Разворот связного списка» целевая сложность O(n). n до 1e6 даёт O(n) времени и O(1) памяти. Рекурсивная форма стоит O(n) стека. ## Разворот связного списка: разбор примера ### Развернуть только часть списка Разверните узлы с позиции left по позицию right. Всё вне этого диапазона сохраняет порядок. Ожидается один проход, копировать значения нельзя. Дойдите до узла перед left и удержите его. Назовём этот узел якорем. Потом перетаскивайте каждый следующий узел в начало развёрнутой части. Якорь держит список связным. ```javascript function reverseBetween(head, left, right) { const dummy = new ListNode(0, head); let anchor = dummy; for (let i = 1; i < left; i++) anchor = anchor.next; const tail = anchor.next; // this node ends up last in the reversed part for (let i = 0; i < right - left; i++) { const moved = tail.next; tail.next = moved.next; moved.next = anchor.next; anchor.next = moved; } return dummy.next; } ``` ## Разворот связного списка: частые ошибки - **Теряют остаток списка** Перезапись current.next без сохранения выбрасывает все последующие узлы. Сохраняйте ссылку заранее. - **Возвращают не тот узел** После цикла текущий равен null. Новая голова это предыдущий. - **Оставляют старую голову висеть** Старая голова обязана в итоге смотреть в null. Старт предыдущего с null даёт это бесплатно. - **Берут рекурсию на длинном списке** Миллион узлов означает миллион кадров стека. Это переполняет стек вызовов. ## Разворот связного списка: задачи с собеседований Паттерн «Разворот связного списка» решает такие задачи с собеседований: Разворот связного списка, Разворот связного списка II, Разворот узлов группами по k, Палиндром в связном списке, Перестройка списка, Обмен узлов парами и Сложение двух чисел II. - **Разворот связного списка** Чистая форма на трёх переменных. - **Разворот связного списка II** Разворачивается только участок, поэтому нужен якорь. - **Разворот узлов группами по k** Разверните каждый блок из k, потом соедините блоки. - **Палиндром в связном списке** Разверните вторую половину и сравните с первой. - **Перестройка списка** Разрежьте, разверните заднюю половину и сплетите обе. - **Обмен узлов парами** Разворот, где k равно двум. - **Сложение двух чисел II** Разверните оба списка, сложите и разверните результат. ## JavaScript ```javascript function reverseList(head) { let prev = null; let curr = head; while (curr) { const next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; } ``` ## TypeScript ```typescript interface ListNode { val: number; next: ListNode | null; } function reverseList(head: ListNode | null): ListNode | null { let prev: ListNode | null = null; let curr = head; while (curr) { const next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; } ``` ## Python ```python def reverse_list(head): prev = None curr = head while curr: nxt = curr.next curr.next = prev prev = curr curr = nxt return prev ``` ## PHP ```php function reverseList(?Node $head): ?Node { $prev = null; $curr = $head; while ($curr !== null) { $next = $curr->next; $curr->next = $prev; $prev = $curr; $curr = $next; } return $prev; } ``` ## Java ```java public static ListNode reverseList(ListNode head) { ListNode prev = null; ListNode curr = head; while (curr != null) { ListNode next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; } public static class ListNode { public int val; public ListNode next; } ``` ## C# ```csharp public static ListNode ReverseList(ListNode head) { ListNode prev = null; ListNode curr = head; while (curr != null) { ListNode next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; } public class ListNode { public int val; public ListNode next; } ``` --- title: "Быстрый и медленный указатели" url: https://algopath.pro/ru/patterns/fast-slow-pointers language: ru summary: "Один указатель делает по одному шагу за раз, а другой сразу по два. Именно разрыв между ними выдаёт циклы и середину списка." updated: 2026-10-17 --- # Быстрый и медленный указатели Один указатель делает по одному шагу за раз, а другой сразу по два. Именно разрыв между ними выдаёт циклы и середину списка. ## Быстрый и медленный указатели: как это работает? Поставьте оба указателя на голову. Один делает шаг за раунд, другой два. Двигайте их вместе в одном цикле. Остановитесь, когда быстрый уйдёт за конец. Если список кончился, цикла нет. Ссылка null это доказательство. Если цикл есть, быстрый указатель обгоняет медленный на круг. Они окажутся на одном узле. Внутри цикла разрыв сокращается на единицу за раунд. Поэтому встреча гарантирована. Когда быстрый доходит до конца, медленный стоит на середине. Середина достаётся бесплатно. Источники: [Википедия: Нахождение цикла](https://ru.wikipedia.org/wiki/%D0%9D%D0%B0%D1%85%D0%BE%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%86%D0%B8%D0%BA%D0%BB%D0%B0) - `slow = 1, fast = 1` Список от 1 до 5. Оба указателя стартуют с головы. - `slow = 2, fast = 3` Один шаг против двух. Разрыв составляет один узел. - `slow = 3, fast = 5` Разрыв стал два. Быстрый почти вышел за конец. - `fast.next равен null` Список кончился, значит цикла нет. - `slow = 3` Медленный стоит на среднем узле. Пять узлов, середина третья. ## Быстрый и медленный указатели: когда применять? Паттерн «Быстрый и медленный указатели» нужен, когда в условии есть слова «обнаружить цикл в связном списке», «найти, где начинается цикл» или «найти средний узел списка». Решение на нём обычно укладывается в O(n). - обнаружить цикл в связном списке - найти, где начинается цикл - найти средний узел списка - определить, является ли список палиндромом - без доп. памяти / O(1) память, только указатели ## Быстрый и медленный указатели: с чем путают? Паттерн «Быстрый и медленный указатели» чаще всего путают с этими паттернами: Два указателя (в одну сторону), Хеш-множество / словарь, Разворот связного списка и Обход графа BFS / DFS. - **Два указателя (в одну сторону)** - Там один указатель читает, а другой пишет. Здесь читают оба, но с разной скоростью. - **Хеш-множество / словарь** - Множество посещённых узлов тоже ловит цикл. Оно стоит O(n) памяти вместо O(1). - **Разворот связного списка** - Разворот перевязывает сами ссылки. Этот приём не меняет в списке ни одного указателя. - **Обход графа BFS / DFS** - Обход находит циклы в любом графе с множеством посещённых. Здесь работает то, что выход из узла один. ## Быстрый и медленный указатели: сложность по времени и памяти У паттерна «Быстрый и медленный указатели» целевая сложность O(n). n до 1e6 даёт O(n) времени и O(1) памяти. Медленный указатель делает не больше n шагов. ## Быстрый и медленный указатели: разбор примера ### Где начинается цикл Связный список может где-то заворачиваться сам в себя. Верните узел, с которого начинается петля. Если петли нет, верните null. Сначала дайте медленному и быстрому указателям встретиться внутри петли. Потом верните один указатель на голову. Двигайте оба по шагу, и они встретятся на входе. ```javascript function detectCycle(head) { let slow = head; let fast = head; while (fast && fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) { // head to entry is the same distance as meeting point to entry let walker = head; while (walker !== slow) { walker = walker.next; slow = slow.next; } return walker; } } return null; } ``` ## Быстрый и медленный указатели: частые ошибки - **Проверяют в условии только быстрый указатель** Чтение fast.next.next падает, когда fast.next равен null. Проверяйте оба до шага. - **Стартуют указатели с разных узлов** Фора меняет узел, на котором они встретятся. Доказательство про вход требует общего старта. - **Сравнивают значения вместо узлов** Два разных узла могут хранить одно значение. Сравнивайте сами ссылки. - **Считают точку встречи входом в цикл** Встреча происходит где-то внутри петли, а не в начале. Вход находит второй проход. ## Быстрый и медленный указатели: задачи с собеседований Паттерн «Быстрый и медленный указатели» решает такие задачи с собеседований: Цикл в связном списке, Цикл в связном списке II, Середина связного списка, Счастливое число, Найти дубликат, Палиндром в связном списке и Удалить n-й узел с конца. - **Цикл в связном списке** Сама встреча и есть весь ответ. - **Цикл в связном списке II** Второй проход от головы находит место входа. - **Середина связного списка** Когда быстрый кончил, медленный стоит на середине. - **Счастливое число** Шаг с суммой квадратов цифр строит невидимый связный список. - **Найти дубликат** Значения массива работают как ссылки на следующий узел. - **Палиндром в связном списке** Найдите середину, разверните заднюю часть, сравните. - **Удалить n-й узел с конца** Здесь фиксированный разрыв, а не двойная скорость. ## JavaScript ```javascript function hasCycle(head) { let slow = head; let fast = head; while (fast && fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) return true; } return false; } ``` ## TypeScript ```typescript interface ListNode { val: number; next: ListNode | null; } function hasCycle(head: ListNode | null): boolean { let slow = head; let fast = head; while (fast && fast.next) { slow = slow!.next; fast = fast.next.next; if (slow === fast) return true; } return false; } ``` ## Python ```python def has_cycle(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: return True return False ``` ## PHP ```php function hasCycle(?Node $head): bool { $slow = $head; $fast = $head; while ($fast !== null && $fast->next !== null) { $slow = $slow->next; $fast = $fast->next->next; if ($slow === $fast) return true; } return false; } ``` ## Java ```java public static boolean hasCycle(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) return true; } return false; } public static class ListNode { public int val; public ListNode next; } ``` ## C# ```csharp public static bool HasCycle(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) return true; } return false; } public class ListNode { public int val; public ListNode next; } ``` --- title: "Слияние и перестройка связного списка" url: https://algopath.pro/ru/patterns/linked-list-merge language: ru summary: "Стройте ответ на фиктивном узле и берите головы из обоих списков. Фиктивный узел убирает разбор случая пустого результата." updated: 2026-10-17 --- # Слияние и перестройка связного списка Стройте ответ на фиктивном узле и берите головы из обоих списков. Фиктивный узел убирает разбор случая пустого результата. ## Слияние и перестройка связного списка: как это работает? Заведите фиктивный узел, который пока никуда не ведёт. Его next и станет настоящей головой. Держите указатель хвоста, начиная с фиктивного узла. Все узлы дописываются туда. Сравните головные узлы обоих списков. Допишите меньший и сдвиньте этот список вперёд. Передвиньте хвост на только что дописанный узел. Повторяйте, пока один список не кончится. Допишите остаток второго списка целиком. Он уже отсортирован, сравнивать нечего. В конце верните dummy.next. Ни один узел не копировался, только перевязывался. Источники: [Wikipedia (англ.): Merge algorithm](https://en.wikipedia.org/wiki/Merge_algorithm) - `dummy, a = 1, b = 2` Сливаем [1, 4] и [2, 3]. Фиктивный узел пока пуст. - `хвост = 1, a = 4, b = 2` 1 меньше 2, поэтому дописывается первой. - `хвост = 2, b = 3` Теперь 4 против 2. Следующей идёт двойка. - `хвост = 3, b = null` Тройка тоже обходит четвёрку. Второй список опустел. - `1, 2, 3, 4` Остаток первого списка дописывается одним шагом. ## Слияние и перестройка связного списка: когда применять? Паттерн «Слияние и перестройка связного списка» нужен, когда в условии есть слова «слить два отсортированных связных списка», «слить k отсортированных списков» или «перестроить список (чередовать переднюю и заднюю половины)». Решение на нём обычно укладывается в O(n + m). - слить два отсортированных связных списка - слить k отсортированных списков - перестроить список (чередовать переднюю и заднюю половины) - переставить узлы без копирования значений в массив - фиктивный головной узел / sentinel ## Слияние и перестройка связного списка: с чем путают? Паттерн «Слияние и перестройка связного списка» чаще всего путают с этими паттернами: Разворот связного списка, Бинарная куча / очередь с приоритетом, Быстрая сортировка (merge / quick) и Быстрый и медленный указатели. - **Разворот связного списка** - Разворот меняет ссылки внутри одного списка. Слияние перевязывает узлы между двумя списками. - **Бинарная куча / очередь с приоритетом** - При k списках куча выбирает наименьшую голову за log k. Для двух списков хватает одного сравнения. - **Быстрая сортировка (merge / quick)** - Шаг слияния в merge sort это ровно этот цикл. Здесь списки приходят уже отсортированными. - **Быстрый и медленный указатели** - Перестройка начинается с поиска середины, а это тот приём. Сплетение после него это уже этот. ## Слияние и перестройка связного списка: сложность по времени и памяти У паттерна «Слияние и перестройка связного списка» целевая сложность O(n + m). n плюс m узлов дают O(n + m) времени и O(1) памяти. Слияние k списков кучей стоит O(N log k). ## Слияние и перестройка связного списка: разбор примера ### Перестроить список с двух концов Переставьте список в порядке первый, последний, второй, предпоследний и так далее. Узлы надо перевязать. Копировать значения в массив нельзя. Найдите середину медленным и быстрым указателем, потом разрежьте список там. Разверните заднюю половину. Сплетите две половины по одному узлу. ```javascript function reorderList(head) { if (!head || !head.next) return head; let slow = head; let fast = head; while (fast.next && fast.next.next) { slow = slow.next; fast = fast.next.next; } let second = slow.next; slow.next = null; // cut the list in two, or the weave loops forever let prev = null; while (second) { const next = second.next; second.next = prev; prev = second; second = next; } let first = head; while (prev) { const a = first.next; const b = prev.next; first.next = prev; prev.next = a; first = a; prev = b; } return head; } ``` ## Слияние и перестройка связного списка: частые ошибки - **Работают без фиктивного узла** Тогда первой дописи нужен отдельный случай для пустого результата. Фиктивный узел убирает эту ветку. - **Теряют остаток** Когда один список кончился, во втором ещё есть узлы. Прицепите весь остаток одним шагом. - **Создают новые узлы** Такие задачи ждут перевязки исходных узлов. Копирование удваивает память впустую. - **Оставляют цикл** Разрезать список значит выставить какой-то next в null. Без этого список зацикливается. ## Слияние и перестройка связного списка: задачи с собеседований Паттерн «Слияние и перестройка связного списка» решает такие задачи с собеседований: Слияние двух отсортированных списков, Слияние k отсортированных списков, Сортировка связного списка, Перестройка списка, Сложение двух чисел, Разбиение списка и Пересечение двух связных списков. - **Слияние двух отсортированных списков** Чистая форма: фиктивный узел и одно сравнение на узел. - **Слияние k отсортированных списков** Куча держит головной узел каждого списка. - **Сортировка связного списка** Разрежьте посередине, отсортируйте половины и слейте. - **Перестройка списка** Разрежьте, разверните заднюю половину и сплетите. - **Сложение двух чисел** Идите по обоим спискам и переносите разряд. - **Разбиение списка** Два фиктивных узла: один для малых значений, другой для остальных. - **Пересечение двух связных списков** Два обходчика, меняющие списки на конце. ## JavaScript ```javascript function mergeTwoLists(a, b) { const dummy = { next: null }; let tail = dummy; while (a && b) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a || b; return dummy.next; } ``` ## TypeScript ```typescript interface ListNode { val: number; next: ListNode | null; } function mergeTwoLists(a: ListNode | null, b: ListNode | null): ListNode | null { const dummy: ListNode = { val: 0, next: null }; let tail = dummy; while (a && b) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a ?? b; return dummy.next; } ``` ## Python ```python def merge_two_lists(a, b): dummy = Node(0) tail = dummy while a and b: if a.val <= b.val: tail.next, a = a, a.next else: tail.next, b = b, b.next tail = tail.next tail.next = a or b return dummy.next ``` ## PHP ```php function mergeTwoLists(?Node $a, ?Node $b): ?Node { $dummy = new Node(0); $tail = $dummy; while ($a !== null && $b !== null) { if ($a->val <= $b->val) { $tail->next = $a; $a = $a->next; } else { $tail->next = $b; $b = $b->next; } $tail = $tail->next; } $tail->next = $a ?? $b; return $dummy->next; } ``` ## Java ```java public static ListNode mergeTwoLists(ListNode a, ListNode b) { ListNode dummy = new ListNode(); ListNode tail = dummy; while (a != null && b != null) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a != null ? a : b; return dummy.next; } public static class ListNode { public int val; public ListNode next; } ``` ## C# ```csharp public static ListNode MergeTwoLists(ListNode a, ListNode b) { var dummy = new ListNode(); var tail = dummy; while (a != null && b != null) { if (a.val <= b.val) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; } tail = tail.next; } tail.next = a ?? b; return dummy.next; } public class ListNode { public int val; public ListNode next; } ``` --- title: "Стек (LIFO)" url: https://algopath.pro/ru/patterns/stack language: ru summary: "Стек всегда отдаёт самый свежий положенный элемент первым. Поэтому он подходит всему, что закрывается в обратном порядке." updated: 2026-10-17 --- # Стек (LIFO) Стек всегда отдаёт самый свежий положенный элемент первым. Поэтому он подходит всему, что закрывается в обратном порядке. ## Стек (LIFO): как это работает? У стека два действия: положить и снять. Последнее положенное возвращается первым. Массива для него вполне достаточно. Вставка пишет в конец, снятие читает и укорачивает. Кладите всё, что открывается. Это может быть скобка, каталог или отложенное вычисление. Снимайте всё, что закрывается. Потом проверьте, что снятое соответствует закрывающему элементу. Пустой стек при снятии значит, что вход испорчен. Непустой стек в конце значит, что что-то не закрыли. Ничего ниже вершины никогда не читается. Именно это ограничение и держит O(1) на операцию. Источники: [Википедия: Стек](https://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BA) - `стек = []` Проверяем строку ([]). Пока ничего не открыто. - `стек = ['(']` Открывающая круглая скобка положена в стек. - `стек = ['(', '[']` Квадратная скобка ложится сверху. - `стек = ['(']` Закрывающая квадратная снимает свою пару. - `стек = []` Закрывающая круглая снимает последнюю. Пустой стек значит верно. ## Стек (LIFO): когда применять? Паттерн «Стек (LIFO)» нужен, когда в условии есть слова «корректные скобки / сбалансированные скобки», «сопоставить самую недавнюю открывающую с ближайшей закрывающей» или «отмена, откат к последнему состоянию, вложенная структура». Решение на нём обычно укладывается в O(n). - корректные скобки / сбалансированные скобки - сопоставить самую недавнюю открывающую с ближайшей закрывающей - отмена, откат к последнему состоянию, вложенная структура - вычислить выражение с вложенными операторами - обработать в порядке, обратном порядку поступления ## Стек (LIFO): с чем путают? Паттерн «Стек (LIFO)» чаще всего путают с этими паттернами: Монотонный стек, Рекурсия, Обход графа BFS / DFS и Монотонная дек-очередь (максимум/минимум окна). - **Монотонный стек** - Там стек упорядочен, и каждое снятие отвечает на вопрос. Обычный стек помнит лишь то, что открыто. - **Рекурсия** - Каждый рекурсивный вызов и так лежит на стеке. Свой стек снимает ограничение по глубине. - **Обход графа BFS / DFS** - Очередь даёт обход в ширину, стек даёт обход в глубину. Тот же обход, другой контейнер. - **Монотонная дек-очередь (максимум/минимум окна)** - Дек читается и подрезается с обоих концов. Стек трогает только вершину. ## Стек (LIFO): сложность по времени и памяти У паттерна «Стек (LIFO)» целевая сложность O(n). Каждая вставка и снятие стоят O(1), значит n операций дают O(n). Память в худшем случае O(n). ## Стек (LIFO): разбор примера ### Упростить путь к файлу Дан абсолютный путь, в котором бывают точки и двойные точки. Верните кратчайший путь с тем же смыслом. Двойная точка поднимает на каталог вверх, одиночная ничего не меняет. Разбейте путь по слешам и идите по частям подряд. Кладите настоящее имя, снимайте на двойной точке, пропускайте пустые части и одиночные точки. В конце склейте стек. ```javascript function simplifyPath(path) { const stack = []; for (const part of path.split("/")) { if (part === "" || part === ".") continue; if (part === "..") { stack.pop(); // popping an empty stack is a no-op at the root } else { stack.push(part); } } return "/" + stack.join("/"); } ``` ## Стек (LIFO): частые ошибки - **Снимают, не проверив на пустоту** Снятие с пустого стека молча возвращает undefined. Проверьте размер, прежде чем верить значению. - **Не смотрят, что осталось в конце** Непустой стек означает, что что-то не закрыли. Проверьте его после цикла. - **Берут shift вместо pop** shift берёт с начала, а это уже очередь. Смысл всего цикла при этом переворачивается. - **Кладут значения, когда нужны позиции** Многие такие задачи спрашивают, как далеко назад что-то было. Кладите индекс и читайте значение по нему. ## Стек (LIFO): задачи с собеседований Паттерн «Стек (LIFO)» решает такие задачи с собеседований: Правильные скобки, Минимальный стек, Обратная польская запись, Упрощение пути, Простой калькулятор, Декодирование строки и Сравнение строк с забоем. - **Правильные скобки** Кладите открывающую, снимайте и сверяйте на закрывающей. - **Минимальный стек** Второй стек текущих минимумов отвечает за O(1). - **Обратная польская запись** Кладите числа, снимайте два на каждой операции. - **Упрощение пути** Кладите каталог, снимайте на двойной точке. - **Простой калькулятор** Кладите текущий результат перед открывающей скобкой. - **Декодирование строки** Кладите число повторов и текст перед каждой скобкой. - **Сравнение строк с забоем** Каждый забой снимает последний сохранённый символ. ## JavaScript ```javascript function isValid(s) { const stack = []; const pairs = { ')': '(', ']': '[', '}': '{' }; for (const ch of s) { if (ch in pairs) { if (stack.pop() !== pairs[ch]) return false; } else { stack.push(ch); } } return stack.length === 0; } ``` ## TypeScript ```typescript function isValid(s: string): boolean { const stack: string[] = []; const pairs: Record = { ')': '(', ']': '[', '}': '{' }; for (const ch of s) { if (ch in pairs) { if (stack.pop() !== pairs[ch]) return false; } else { stack.push(ch); } } return stack.length === 0; } ``` ## Python ```python def is_valid(s): stack = [] pairs = {')': '(', ']': '[', '}': '{'} for ch in s: if ch in pairs: if not stack or stack.pop() != pairs[ch]: return False else: stack.append(ch) return not stack ``` ## PHP ```php function isValid(string $s): bool { $stack = []; $pairs = [')' => '(', ']' => '[', '}' => '{']; foreach (str_split($s) as $ch) { if (isset($pairs[$ch])) { if (array_pop($stack) !== $pairs[$ch]) return false; } else { $stack[] = $ch; } } return count($stack) === 0; } ``` ## Java ```java public static boolean isValid(String s) { Deque stack = new ArrayDeque<>(); Map pairs = Map.of(')', '(', ']', '[', '}', '{'); for (char ch : s.toCharArray()) { if (pairs.containsKey(ch)) { if (stack.isEmpty() || stack.pop() != pairs.get(ch)) return false; } else { stack.push(ch); } } return stack.isEmpty(); } ``` ## C# ```csharp public static bool IsValid(string s) { var stack = new Stack(); var pairs = new Dictionary { [')'] = '(', [']'] = '[', ['}'] = '{', }; foreach (char ch in s) { if (pairs.ContainsKey(ch)) { if (stack.Count == 0 || stack.Pop() != pairs[ch]) return false; } else { stack.Push(ch); } } return stack.Count == 0; } ``` --- title: "Монотонный стек" url: https://algopath.pro/ru/patterns/monotonic-stack language: ru summary: "Держите значения в стеке убывающими снизу вверх. Снимайте всё, что проигрывает новому, и каждое снятие находит свой ответ." updated: 2026-10-22 --- # Монотонный стек Держите значения в стеке убывающими снизу вверх. Снимайте всё, что проигрывает новому, и каждое снятие находит свой ответ. ## Монотонный стек: как это работает? Стек хранит индексы, а не значения. Каждый индекс ждёт там своего ответа. Монотонный значит, что значения по индексам в стеке меняются только в одну сторону. Для следующего большего они не растут снизу вверх. Перед добавлением нового индекса снимите все, чьи значения он превосходит. Каждое снятие даёт ответ. Направление сравнения решает результат. Снимайте, пока верхушка меньше, и получите следующий больший. Разверните сравнение на «пока верхушка больше». Так получается следующий меньший. Для предыдущего большего снимайте, пока значение на верхушке не больше нового. Индекс, оставшийся наверху, и есть ответ, а пустой стек значит, что его нет. Всё, что осталось в стеке, ответа не нашло. У этих позиций остаётся значение по умолчанию. Внутренний цикл выглядит квадратичным. Каждый индекс входит и выходит по разу, поэтому итог линейный. Источники: [Wikipedia (англ.): All nearest smaller values](https://en.wikipedia.org/wiki/All_nearest_smaller_values) - `[0]` Кладём индекс 0. Значение 2 лежит в стеке. - `[0, 1]` Значение 1 проигрывает двойке. Снимать нечего, кладём индекс 1. - `[2]` Значение 5 бьёт 1, затем 2. Оба снимаются с ответом 5. - `[2, 3]` Значение 3 проигрывает пятёрке. Кладём индекс 3. - `[2, 3]` Вход кончился. У индексов 2 и 3 остаётся -1. ## Монотонный стек: когда применять? Паттерн «Монотонный стек» нужен, когда в условии есть слова «ближайший больший / ближайший меньший элемент», «сколько дней ждать потепления» или «наибольший прямоугольник в гистограмме». Решение на нём обычно укладывается в O(n). - ближайший больший / ближайший меньший элемент - сколько дней ждать потепления - наибольший прямоугольник в гистограмме - сколько воды соберётся между столбиками - отрезок/серия, заканчивающаяся первым большим или меньшим значением ## Монотонный стек: с чем путают? Паттерн «Монотонный стек» чаще всего путают с этими паттернами: Стек, Монотонный дек, Скользящее окно (переменное), Двоичная куча и Сортировка с компаратором. - **Стек** - Обычный стек снимает по вашей команде. Монотонный снимает по сравнению. - **Монотонный дек** - Дек ещё и выбрасывает элементы, выпавшие из окна. Если никто не устаревает, хватит стека. - **Скользящее окно (переменное)** - Окно отвечает на вопросы про диапазон. Этот стек отвечает про один граничный элемент. - **Двоичная куча** - Куча даёт глобальный максимум за O(log n). Стек даёт ближайшего большего соседа за O(1). - **Сортировка с компаратором** - Сортировка выбрасывает позиции. Вопросы про следующий больший целиком про позиции. ## Монотонный стек: сложность по времени и памяти У паттерна «Монотонный стек» целевая сложность O(n). n до 1e5 элементов даёт O(n). Каждый элемент кладут один раз и снимают не больше раза. ## Монотонный стек: разбор примера ### Температура по дням Дан список дневных температур. Для каждого дня посчитайте, сколько дней до более тёплого. Если тёплого дня нет, ответ 0. Вложенные циклы дают O(n в квадрате) и не проходят по времени. Задача о температурах это тот же следующий больший элемент, только нужна дистанция вместо значения. Идём слева направо. Снимаем каждый день в стеке, который холоднее сегодняшнего. Сегодня и есть его первый тёплый день. Ожидание равно разнице индексов. Дни, оставшиеся в стеке, тепла не дождались. У них остаётся 0. ```javascript function dailyTemperatures(temps) { const wait = new Array(temps.length).fill(0); const stack = []; // indices, coldest at the bottom for (let today = 0; today < temps.length; today++) { while ( stack.length && temps[stack[stack.length - 1]] < temps[today] ) { const colder = stack.pop(); wait[colder] = today - colder; } stack.push(today); } return wait; // days still on the stack keep their 0 } ``` ## Монотонный стек: частые ошибки - **Кладут значения вместо индексов** Для расстояния нужна позиция. Кладите индекс, а значение берите как temps[i]. - **Путают направление сравнения** Меньше на верхушке даёт следующий больший. Больше на верхушке даёт следующий меньший. - **Забывают про остаток стека** Элементы, оставшиеся в стеке, ответа не нашли. Решите заранее: -1, 0 или длина массива. - **Неверно обрабатывают равные значения** Строгое < оставляет дубликаты в стеке, <= снимает их. Проверьте на [2, 2, 2]. ## Монотонный стек: задачи с собеседований Паттерн «Монотонный стек» решает такие задачи с собеседований: Следующий больший элемент, Температура по дням, Наибольший прямоугольник в гистограмме, Сбор дождевой воды, Биржевой спан, Удалить k цифр и Сумма минимумов подмассивов. - **Следующий больший элемент** Паттерн без маскировки. Остальные задачи переформулируют его. - **Температура по дням** Тот же следующий больший, но спрашивают дистанцию. - **Наибольший прямоугольник в гистограмме** Каждому столбцу нужен первый меньший сосед с обеих сторон. - **Сбор дождевой воды** Вода стоит между столбцом и следующим более высоким. - **Биржевой спан** Следующий больший в обратную сторону, со счётом дней. - **Удалить k цифр** Снимаем большие цифры, чтобы осталось наименьшее число. - **Сумма минимумов подмассивов** Каждое значение владеет отрезком между меньшими соседями. ## JavaScript ```javascript function nextGreater(nums) { const result = new Array(nums.length).fill(-1); const stack = []; for (let i = 0; i < nums.length; i++) { while (stack.length && nums[stack[stack.length - 1]] < nums[i]) { result[stack.pop()] = nums[i]; } stack.push(i); } return result; } ``` ## TypeScript ```typescript function nextGreater(nums: number[]): number[] { const result: number[] = new Array(nums.length).fill(-1); const stack: number[] = []; for (let i = 0; i < nums.length; i++) { while (stack.length && nums[stack[stack.length - 1]] < nums[i]) { result[stack.pop() as number] = nums[i]; } stack.push(i); } return result; } ``` ## Python ```python def next_greater(nums): result = [-1] * len(nums) stack = [] for i, x in enumerate(nums): while stack and nums[stack[-1]] < x: result[stack.pop()] = x stack.append(i) return result ``` ## PHP ```php function nextGreater(array $nums): array { $result = array_fill(0, count($nums), -1); $stack = []; foreach ($nums as $i => $x) { while ($stack && $nums[end($stack)] < $x) { $result[array_pop($stack)] = $x; } $stack[] = $i; } return $result; } ``` ## Java ```java public static int[] nextGreater(int[] nums) { int[] result = new int[nums.length]; Arrays.fill(result, -1); Deque stack = new ArrayDeque<>(); for (int i = 0; i < nums.length; i++) { while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) { result[stack.pop()] = nums[i]; } stack.push(i); } return result; } ``` ## C# ```csharp public static int[] NextGreater(int[] nums) { var result = new int[nums.Length]; Array.Fill(result, -1); var stack = new Stack(); for (int i = 0; i < nums.Length; i++) { while (stack.Count > 0 && nums[stack.Peek()] < nums[i]) { result[stack.Pop()] = nums[i]; } stack.Push(i); } return result; } ``` --- title: "Монотонная дек-очередь (максимум/минимум окна)" url: https://algopath.pro/ru/patterns/monotonic-deque language: ru summary: "Дек, упорядоченный так, что спереди всегда лежит максимум окна. Проигравшие значения уходят сзади, а устаревшие спереди." updated: 2026-10-17 --- # Монотонная дек-очередь (максимум/минимум окна) Дек, упорядоченный так, что спереди всегда лежит максимум окна. Проигравшие значения уходят сзади, а устаревшие спереди. ## Монотонная дек-очередь (максимум/минимум окна): как это работает? Дек хранит индексы, а не значения. Индекс говорит, когда элемент выйдет из окна. Перед вставкой нового индекса снимите сзади все индексы с меньшим значением. Они уже никогда не станут максимумом. Положите новый индекс в конец. Значения теперь убывают от начала к концу. Посмотрите на передний индекс. Если он выехал из окна, уберите его. Теперь спереди лежит максимум окна. Читайте его без всякого перебора. Каждый индекс кладётся один раз и снимается один раз. Именно это держит проход линейным. Источники: [Википедия: Двухсторонняя очередь](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D1%83%D1%85%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8F%D1%8F_%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C), [Документация Python (англ.): collections.deque](https://docs.python.org/3/library/collections.html#collections.deque) - `дек = [0]` Окно из 3 по массиву [1, 3, -1, -3, 5]. Индекс 0 входит. - `дек = [1]` Тройка бьёт стоящую сзади единицу. Индекс 0 снимается с конца. - `дек = [1, 2]` -1 меньше, поэтому остаётся. Окно заполнено, максимум равен 3. - `дек = [1, 2, 3]` -3 снова меньше. Передний индекс всё ещё в окне. - `дек = [4]` Пятёрка бьёт всех и опустошает дек. Максимум равен 5. ## Монотонная дек-очередь (максимум/минимум окна): когда применять? Паттерн «Монотонная дек-очередь (максимум/минимум окна)» нужен, когда в условии есть слова «максимум или минимум каждого окна размера k», «максимум/минимум скользящего окна» или «кратчайший подмассив с суммой не менее K». Решение на нём обычно укладывается в O(n). - максимум или минимум каждого окна размера k - максимум/минимум скользящего окна - кратчайший подмассив с суммой не менее K - нужно снимать/класть с обоих концов очереди - дек, двусторонняя очередь ## Монотонная дек-очередь (максимум/минимум окна): с чем путают? Паттерн «Монотонная дек-очередь (максимум/минимум окна)» чаще всего путают с этими паттернами: Монотонный стек, Скользящее окно (фиксированный размер), Бинарная куча / очередь с приоритетом и Стек (LIFO). - **Монотонный стек** - Стек снимает только с вершины. Дек снимает ещё и спереди, когда значение выходит из окна. - **Скользящее окно (фиксированный размер)** - Скользящая сумма переживает вычитание. Максимум нет, ради этого дек и нужен. - **Бинарная куча / очередь с приоритетом** - Куча отдаёт максимум за log n. Она не умеет удалить элемент, только что вышедший из окна. - **Стек (LIFO)** - Обычный стек не хранит порядка и открыт с одного конца. Оба ограничения тут мешают. ## Монотонная дек-очередь (максимум/минимум окна): сложность по времени и памяти У паттерна «Монотонная дек-очередь (максимум/минимум окна)» целевая сложность O(n). n до 1e6 даёт O(n), ведь каждый индекс входит и выходит один раз. Память O(k) по размеру окна. ## Монотонная дек-очередь (максимум/минимум окна): разбор примера ### Самый длинный отрезок в пределах лимита Найдите самый длинный отрезок, где наибольшее и наименьшее значения различаются не больше чем на лимит. Отрезок должен быть непрерывным. Растите переменное окно и держите два дека: для максимумов и для минимумов. Когда два передних значения расходятся больше лимита, сужайте слева. Передний индекс убирайте, как только он устарел. ```javascript function longestSubarray(nums, limit) { const maxQ = []; // indices, values falling from front to back const minQ = []; // indices, values rising from front to back let left = 0; let best = 0; for (let right = 0; right < nums.length; right++) { while (maxQ.length && nums[maxQ[maxQ.length - 1]] <= nums[right]) maxQ.pop(); while (minQ.length && nums[minQ[minQ.length - 1]] >= nums[right]) minQ.pop(); maxQ.push(right); minQ.push(right); while (nums[maxQ[0]] - nums[minQ[0]] > limit) { if (maxQ[0] === left) maxQ.shift(); if (minQ[0] === left) minQ.shift(); left++; } best = Math.max(best, right - left + 1); } return best; } ``` ## Монотонная дек-очередь (максимум/минимум окна): частые ошибки - **Хранят значения вместо индексов** Без индекса не понять, когда элемент устарел. Кладите позицию и читайте значение по ней. - **Подрезают перед вставкой не тот конец** Порядок здесь важен. Сначала снимите проигравших сзади, потом проверьте начало. - **Небрежно обходятся с равными значениями** Снимать ли при равенстве, решает, какой из дубликатов выживет. Это важно, когда ответ это индекс. - **Берут кучу вместо дека** Куча не умеет удалить элемент, только что вышедший из окна. Её удаления ленивые и стоят лишнего. ## Монотонная дек-очередь (максимум/минимум окна): задачи с собеседований Паттерн «Монотонная дек-очередь (максимум/минимум окна)» решает такие задачи с собеседований: Максимум скользящего окна, Отрезок с разбросом в пределах лимита, Кратчайший подмассив с суммой не меньше k, Прыжки VI, Сумма подпоследовательности с ограничением, Максимум значения уравнения и Минимум скользящего окна. - **Максимум скользящего окна** Чистая форма: читайте начало на каждом шаге. - **Отрезок с разбросом в пределах лимита** Два дека, по одному на каждый край. - **Кратчайший подмассив с суммой не меньше k** Префиксные суммы и возрастающий дек по ним. - **Прыжки VI** Лучший результат внутри достижимого окна. - **Сумма подпоследовательности с ограничением** То же окно, но по массиву динамики. - **Максимум значения уравнения** Окно по x с ключом y минус x. - **Минимум скользящего окна** Тот же код с перевёрнутым сравнением. ## JavaScript ```javascript function maxSlidingWindow(nums, k) { const deque = []; // stores indices, values decreasing const result = []; for (let i = 0; i < nums.length; i++) { while (deque.length && deque[0] <= i - k) deque.shift(); while (deque.length && nums[deque[deque.length - 1]] < nums[i]) deque.pop(); deque.push(i); if (i >= k - 1) result.push(nums[deque[0]]); } return result; } ``` ## TypeScript ```typescript function maxSlidingWindow(nums: number[], k: number): number[] { const deque: number[] = []; // stores indices, values decreasing const result: number[] = []; for (let i = 0; i < nums.length; i++) { while (deque.length && deque[0] <= i - k) deque.shift(); while (deque.length && nums[deque[deque.length - 1]] < nums[i]) deque.pop(); deque.push(i); if (i >= k - 1) result.push(nums[deque[0]]); } return result; } ``` ## Python ```python from collections import deque as dq def max_sliding_window(nums, k): d = dq() # indices, values decreasing result = [] for i, x in enumerate(nums): while d and d[0] <= i - k: d.popleft() while d and nums[d[-1]] < x: d.pop() d.append(i) if i >= k - 1: result.append(nums[d[0]]) return result ``` ## PHP ```php function maxSlidingWindow(array $nums, int $k): array { $deque = []; // indices, values decreasing $result = []; foreach ($nums as $i => $x) { while ($deque && $deque[0] <= $i - $k) array_shift($deque); while ($deque && $nums[end($deque)] < $x) array_pop($deque); $deque[] = $i; if ($i >= $k - 1) $result[] = $nums[$deque[0]]; } return $result; } ``` ## Java ```java public static int[] maxSlidingWindow(int[] nums, int k) { Deque deque = new ArrayDeque<>(); // indices, values decreasing int[] result = new int[nums.length - k + 1]; int out = 0; for (int i = 0; i < nums.length; i++) { while (!deque.isEmpty() && deque.peekFirst() <= i - k) deque.pollFirst(); while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) deque.pollLast(); deque.addLast(i); if (i >= k - 1) result[out++] = nums[deque.peekFirst()]; } return result; } ``` ## C# ```csharp public static int[] MaxSlidingWindow(int[] nums, int k) { var deque = new LinkedList(); // indices, values decreasing var result = new int[nums.Length - k + 1]; int outIndex = 0; for (int i = 0; i < nums.Length; i++) { while (deque.Count > 0 && deque.First.Value <= i - k) deque.RemoveFirst(); while (deque.Count > 0 && nums[deque.Last.Value] < nums[i]) deque.RemoveLast(); deque.AddLast(i); if (i >= k - 1) result[outIndex++] = nums[deque.First.Value]; } return result; } ``` --- title: "Рекурсия" url: https://algopath.pro/ru/patterns/recursion language: ru summary: "Функция вызывает саму себя на уменьшенной версии задачи. Базовый случай её останавливает, а вызовы разворачиваются назад." updated: 2026-10-17 --- # Рекурсия Функция вызывает саму себя на уменьшенной версии задачи. Базовый случай её останавливает, а вызовы разворачиваются назад. ## Рекурсия: как это работает? Найдите наименьший случай, на который можно ответить без работы. Это и есть базовый случай. Считайте, что функция уже работает на меньшем входе. Ей доверяют, а не прослеживают её. Напишите один шаг, уменьшающий задачу. Потом вызовите себя на остатке. Соедините вернувшийся результат с этим одним шагом. Эти две строки и есть всё тело. Каждый вызов занимает кадр памяти. Глубина n значит n живых кадров одновременно. Без базового случая вызовы не кончаются. Стек заполняется и программа падает. Источники: [Википедия: Рекурсия](https://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D1%8F) - `sum([2, 4, 6])` Список не пуст. Берём двойку и спрашиваем про остаток. - `sum([4, 6])` Берём четвёрку и спрашиваем снова. Список продолжает уменьшаться. - `sum([6])` Остался один элемент. Это всё ещё не базовый случай. - `sum([]) = 0` Пустой список это базовый случай. Он отвечает без работы. - `0, 6, 10, 12` Результаты разворачиваются наверх. Каждый вызов добавляет своё значение. ## Рекурсия: когда применять? Паттерн «Рекурсия» нужен, когда в условии есть слова «задача определена через меньшую версию самой себя», «вложенная/древовидная структура для обхода (папки, выражение, связный список)» или «нужно посчитать сумму/количество/глубину по всем подэлементам». Решение на нём обычно укладывается в O(n). - задача определена через меньшую версию самой себя - вложенная/древовидная структура для обхода (папки, выражение, связный список) - нужно посчитать сумму/количество/глубину по всем подэлементам - естественное деление на базовый случай и рекурсивный (факториал, Фибоначчи, глубина дерева) ## Рекурсия: с чем путают? Паттерн «Рекурсия» чаще всего путают с этими паттернами: Рекурсия с мемоизацией, Бэктрекинг, Стек (LIFO) и Обход дерева. - **Рекурсия с мемоизацией** - Мемоизация это то же самое плюс кеш прошлых ответов. Она нужна, когда аргумент повторяется. - **Бэктрекинг** - Бэктрекинг отменяет выбор после того, как его исследовал. Обычная рекурсия ничего не отменяет. - **Стек (LIFO)** - Стек вызовов это стек, который вы не писали. Свой стек снимает ограничение по глубине. - **Обход дерева** - На деревьях рекурсия дешевле и понятнее всего. Та страница про три порядка посещения. ## Рекурсия: сложность по времени и памяти У паттерна «Рекурсия» целевая сложность O(n). Глубина примерно до 10000 в браузере безопасна. Глубже стоит переписать рекурсию циклом. ## Рекурсия: разбор примера ### Развернуть вложенный массив Дан массив, элементы которого это числа или другие массивы. Верните один плоский массив чисел. Вложенность бывает любой глубины. Число это базовый случай, оно возвращается как есть. Массив это меньшая задача: разверните каждый элемент и склейте куски. Глубина разбирается сама. ```javascript function flatten(items) { const result = []; for (const item of items) { if (Array.isArray(item)) { // trust the call to handle every depth below this one result.push(...flatten(item)); } else { result.push(item); } } return result; } ``` ## Рекурсия: частые ошибки - **Базового случая нет или он неверный** Тогда вызовы идут, пока не заполнится стек. Пишите базовый случай раньше всего остального. - **Вход не уменьшается** Каждый вызов обязан приближать к базовому случаю. Тот же аргумент даёт бесконечный цикл. - **Уходят слишком глубоко** Списку из миллиона узлов нужен миллион кадров. Такое переписывают циклом. - **Делят один изменяемый массив между вызовами** Вложенный вызов может испортить то, что нужно родителю. Копируйте или откатывайте изменение на выходе. ## Рекурсия: задачи с собеседований Паттерн «Рекурсия» решает такие задачи с собеседований: Факториал и числа Фибоначчи, Разворот строки, Разворачивание вложенного списка, Слияние двух отсортированных списков, Возведение в степень, Ханойские башни и Глубина двоичного дерева. - **Факториал и числа Фибоначчи** Классическая форма, хотя Фибоначчи требует кеша. - **Разворот строки** Поменяйте концы местами и спуститесь в середину. - **Разворачивание вложенного списка** Базовый случай это обычное значение. - **Слияние двух отсортированных списков** Возьмите меньшую голову и спуститесь в остаток. - **Возведение в степень** Делите показатель пополам вместо вычитания единицы. - **Ханойские башни** Два меньших перемещения вокруг одного прямого. - **Глубина двоичного дерева** Единица плюс более глубокий из двух потомков. ## JavaScript ```javascript function recurse(n) { if (n <= 0) return 0; // base case return n + recurse(n - 1); // recursive case: smaller subproblem } ``` ## TypeScript ```typescript function recurse(n: number): number { if (n <= 0) return 0; // base case return n + recurse(n - 1); // recursive case: smaller subproblem } ``` ## Python ```python def recurse(n): if n <= 0: return 0 # base case return n + recurse(n - 1) # recursive case: smaller subproblem ``` ## PHP ```php function recurse(int $n): int { if ($n <= 0) return 0; // base case return $n + recurse($n - 1); // recursive case: smaller subproblem } ``` ## Java ```java public static int recurse(int n) { if (n <= 0) return 0; // base case return n + recurse(n - 1); // recursive case: smaller subproblem } ``` ## C# ```csharp public static int Recurse(int n) { if (n <= 0) return 0; // base case return n + Recurse(n - 1); // recursive case: smaller subproblem } ``` --- title: "Рекурсия с мемоизацией" url: https://algopath.pro/ru/patterns/memoization language: ru summary: "Рекурсия, которая записывает каждый посчитанный ответ. Когда тот же аргумент приходит снова, отдаётся сохранённое значение." updated: 2026-10-17 --- # Рекурсия с мемоизацией Рекурсия, которая записывает каждый посчитанный ответ. Когда тот же аргумент приходит снова, отдаётся сохранённое значение. ## Рекурсия с мемоизацией: как это работает? Сначала напишите обычную рекурсию. Добейтесь правильности до попыток ускорить. Посмотрите, что на самом деле является аргументами. Если два вызова их делят, ответ у них общий. Добавьте кеш с ключом по этим аргументам. Подойдёт и словарь, и обычный массив. В начале функции возвращайте сохранённый ответ, если он есть. Дальше ничего не выполняется. Перед возвратом сохраните ответ под его ключом. Следующий вызов прочитает его вместо пересчёта. Теперь каждый различный аргумент считается один раз. Дерево вызовов схлопывается в граф. Источники: [Википедия: Мемоизация](https://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D0%BC%D0%BE%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F), [Документация Python (англ.): functools.cache](https://docs.python.org/3/library/functools.html#functools.cache) - `fib(5) требует fib(4) и fib(3)` Наивная версия расходится на два вызова. - `fib(4) требует fib(3) и fib(2)` Теперь fib(3) нужен уже дважды. - `кеш = {2: 1, 3: 2}` Первый fib(3) посчитан и сохранён. - `fib(3) это чтение из кеша` Второй fib(3) читает кеш. Всё его поддерево пропущено. - `15 вызовов стали 9` Без кеша fib(50) занял бы миллиарды вызовов. ## Рекурсия с мемоизацией: когда применять? Паттерн «Рекурсия с мемоизацией» нужен, когда в условии есть слова «рекурсивные вызовы повторяют одни и те же аргументы (пересекающиеся подзадачи)», «обычная рекурсия работает верно, но слишком медленно / экспоненциальный взрыв» или «мало различных состояний, хотя дерево рекурсии огромное». Решение на нём обычно укладывается в O(states). - рекурсивные вызовы повторяют одни и те же аргументы (пересекающиеся подзадачи) - обычная рекурсия работает верно, но слишком медленно / экспоненциальный взрыв - мало различных состояний, хотя дерево рекурсии огромное - закешировать результат (i, ...) перед возвратом - формулировка вроде Фибоначчи / подъём по лестнице / путь по сетке с намёком на кеш ## Рекурсия с мемоизацией: с чем путают? Паттерн «Рекурсия с мемоизацией» чаще всего путают с этими паттернами: Рекурсия, Динамическое программирование (1-D), Хеш-множество / словарь и Бэктрекинг. - **Рекурсия** - Обычная рекурсия каждый раз пересчитывает повторный аргумент. Кеш это вся разница. - **Динамическое программирование (1-D)** - Динамика снизу заполняет таблицу в заданном порядке. Здесь она заполняется по требованию, сверху. - **Хеш-множество / словарь** - Кешем обычно служит словарь. Эта страница про то, когда его вообще заводить. - **Бэктрекинг** - Бэктрекинг обходит пути, которые все разные, поэтому ничего не повторяется. Кеш там хранит ключи, которых больше не спросят. ## Рекурсия с мемоизацией: сложность по времени и памяти У паттерна «Рекурсия с мемоизацией» целевая сложность O(states). Стоимость это число различных аргументов на работу одного вызова. Кеш в 1e6 ключей нормален. ## Рекурсия с мемоизацией: разбор примера ### Наименьшее число монет на сумму Даны номиналы монет и сумма. Верните наименьшее число монет, дающее эту сумму. Если набрать сумму нельзя, верните минус один. Задайте тот же вопрос про меньшую сумму. Каждая монета даёт одну ветку. Подзадачу определяет только остаток суммы. Значит по нему и делается ключ кеша. ```javascript function coinChange(coins, amount) { const cache = new Map(); function best(left) { if (left === 0) return 0; if (left < 0) return Infinity; if (cache.has(left)) return cache.get(left); // computed before let answer = Infinity; for (const coin of coins) { answer = Math.min(answer, best(left - coin) + 1); } cache.set(left, answer); // store the failure too return answer; } const result = best(amount); return result === Infinity ? -1 : result; } ``` ## Рекурсия с мемоизацией: частые ошибки - **В ключ не попал один из аргументов** В ключ входит всё, от чего зависит ответ. Неполный ключ вернёт ответ другого вызова. - **Кешируют изменяемый объект** Сохранённую ссылку может испортить более поздний вызов. Храните копию или простое значение. - **Не кешируют неудачу** Провалившаяся ветка это тоже ответ, который стоит сохранить. Иначе тупики обходятся заново. - **Кешируют там, где ничего не повторяется** Если все аргументы разные, кеш только тратит память. Сначала проверьте пересечение подзадач. ## Рекурсия с мемоизацией: задачи с собеседований Паттерн «Рекурсия с мемоизацией» решает такие задачи с собеседований: Числа Фибоначчи, Подъём по лестнице, Размен монет, Ограбление домов, Разбиение строки на слова, Число путей в сетке и Самый длинный возрастающий путь в матрице. - **Числа Фибоначчи** Кратчайший пример пересекающихся подзадач. - **Подъём по лестнице** Та же форма с другим базовым случаем. - **Размен монет** Ключ это оставшаяся сумма. - **Ограбление домов** Ключ это индекс, на котором вы стоите. - **Разбиение строки на слова** Ключ это позиция начала в строке. - **Число путей в сетке** Ключ это пара координат. - **Самый длинный возрастающий путь в матрице** Ключ это клетка, и множество посещённых не нужно. ## JavaScript ```javascript function fib(n, memo = new Map()) { if (n <= 1) return n; if (memo.has(n)) return memo.get(n); const result = fib(n - 1, memo) + fib(n - 2, memo); memo.set(n, result); return result; } ``` ## TypeScript ```typescript function fib(n: number, memo: Map = new Map()): number { if (n <= 1) return n; const cached = memo.get(n); if (cached !== undefined) return cached; const result = fib(n - 1, memo) + fib(n - 2, memo); memo.set(n, result); return result; } ``` ## Python ```python def fib(n, memo=None): if memo is None: memo = {} if n <= 1: return n if n in memo: return memo[n] memo[n] = fib(n - 1, memo) + fib(n - 2, memo) return memo[n] ``` ## PHP ```php function fib(int $n, array &$memo = []): int { if ($n <= 1) return $n; if (isset($memo[$n])) return $memo[$n]; return $memo[$n] = fib($n - 1, $memo) + fib($n - 2, $memo); } ``` ## Java ```java public static long fib(int n, Map memo) { if (n <= 1) return n; Long cached = memo.get(n); if (cached != null) return cached; long result = fib(n - 1, memo) + fib(n - 2, memo); memo.put(n, result); return result; } ``` ## C# ```csharp public static long Fib(int n, Dictionary memo) { if (n <= 1) return n; if (memo.TryGetValue(n, out long cached)) return cached; long result = Fib(n - 1, memo) + Fib(n - 2, memo); memo[n] = result; return result; } ``` --- title: "Бэктрекинг" url: https://algopath.pro/ru/patterns/backtracking language: ru summary: "Сделайте выбор, спуститесь вниз, а потом отмените выбор. Именно отмена позволяет одному массиву держать все варианты подряд." updated: 2026-10-17 --- # Бэктрекинг Сделайте выбор, спуститесь вниз, а потом отмените выбор. Именно отмена позволяет одному массиву держать все варианты подряд. ## Бэктрекинг: как это работает? Держите один массив со сделанными выборами. Это путь вниз по дереву. На каждом уровне переберите оставшиеся варианты. Каждый вариант это одна ветка. Добавьте вариант и вызовите себя для следующего уровня. Больше ничего не меняется. Когда вызов вернулся, снимите вариант обратно. Массив стал ровно таким, каким был. Внизу запишите копию пути. Запись самого массива сохранит общий буфер. Обрезайте ветку сразу, как только она обречена. Именно обрезка и даёт скорость. Источники: [Википедия: Поиск с возвратом](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%B8%D1%81%D0%BA_%D1%81_%D0%B2%D0%BE%D0%B7%D0%B2%D1%80%D0%B0%D1%82%D0%BE%D0%BC) - `путь = []` Перечисляем подмножества [1, 2]. Пустое уже считается ответом. - `путь = [1]` Берём единицу и спускаемся на уровень ниже. - `путь = [1, 2]` Берём и двойку. Эта ветка закончена. - `путь = [1], потом []` Два снятия отменяют оба выбора. Массив снова пуст. - `путь = [2]` Пропускаем единицу и берём двойку. Всего четыре подмножества. ## Бэктрекинг: когда применять? Паттерн «Бэктрекинг» нужен, когда в условии есть слова «сгенерировать все подмножества/перестановки/комбинации», «n маленькое (примерно n <= 12-20), экспоненциальный перебор допустим» или «сделать выбор, уйти в рекурсию, затем отменить выбор перед следующим». Решение на нём обычно укладывается в O(2^n)/O(n!). - сгенерировать все подмножества/перестановки/комбинации - n маленькое (примерно n <= 12-20), экспоненциальный перебор допустим - сделать выбор, уйти в рекурсию, затем отменить выбор перед следующим - расставить элементы с ограничениями и откатиться при конфликте (N ферзей, судоку) - вернуть все допустимые варианты, а не один ## Бэктрекинг: с чем путают? Паттерн «Бэктрекинг» чаще всего путают с этими паттернами: Рекурсия, Рекурсия с мемоизацией, Динамическое программирование (1-D) и Поиск слова в матрице (DFS/backtracking по сетке). - **Рекурсия** - Рекурсия спускается и возвращается. Бэктрекинг ещё и возвращает состояние назад при выходе. - **Рекурсия с мемоизацией** - Кеш окупается, когда аргументы повторяются. На пути из разных выборов это бывает редко. - **Динамическое программирование (1-D)** - Динамика считает или оптимизирует, ничего не перечисляя. Бэктрекинг нужен, когда нужны сами варианты. - **Поиск слова в матрице (DFS/backtracking по сетке)** - Это тот же приём на сетке. Выбором там служит, в какого соседа шагнуть. ## Бэктрекинг: сложность по времени и памяти У паттерна «Бэктрекинг» целевая сложность O(2^n)/O(n!). Ответ растёт экспоненциально, поэтому n маленькое. Подмножеств из 20 элементов 1e6, перестановок из 10 около 3.6e6. ## Бэктрекинг: разбор примера ### Все комбинации, дающие сумму Даны различные положительные числа и целевая сумма. Перечислите все комбинации, дающие её. Число можно брать сколько угодно раз. На каждом уровне пробуйте все числа начиная с текущего индекса. Старт с текущего индекса не даёт одной комбинации повториться в другом порядке. Отрицательный остаток обрезает ветку. ```javascript function combinationSum(candidates, target) { const result = []; const path = []; function walk(start, left) { if (left === 0) { result.push([...path]); // a copy, never the live array return; } if (left < 0) return; // pruned: this branch cannot recover for (let i = start; i < candidates.length; i++) { path.push(candidates[i]); walk(i, left - candidates[i]); // i, not i + 1: reuse is allowed path.pop(); // undo before the next option } } walk(0, target); return result; } ``` ## Бэктрекинг: частые ошибки - **Кладут в ответ живой путь** Тогда все результаты ссылаются на один массив. Кладите его копию. - **Забывают отменить выбор** Путь растёт и не сокращается. Поздние ветки наследуют чужие выборы. - **Начинают внутренний цикл с нуля** Тогда одна комбинация появляется во всех возможных порядках. Передавайте текущий индекс вниз. - **Никогда не обрезают ветки** Без раннего выхода обходится всё дерево целиком. Большинство таких задач быстры именно за счёт обрезки. ## Бэктрекинг: задачи с собеседований Паттерн «Бэктрекинг» решает такие задачи с собеседований: Подмножества, Перестановки, Комбинации с заданной суммой, Ферзи на доске, Поиск слова в сетке, Разбиение на палиндромы и Решатель судоку. - **Подмножества** Два варианта на элемент: взять или пропустить. - **Перестановки** На каждом уровне вариант это любой неиспользованный элемент. - **Комбинации с заданной суммой** Переданный вниз индекс решает, можно ли брать повторно. - **Ферзи на доске** Обрезка идёт по столбцу и по обеим диагоналям. - **Поиск слова в сетке** Сетка это дерево, а соседи это варианты. - **Разбиение на палиндромы** Режьте в каждой позиции, где префикс палиндром. - **Решатель судоку** Девять вариантов на пустую клетку, обрезанных правилами. ## JavaScript ```javascript function backtrack(path, choices, result) { if (path.length === choices.length) { // or another stop condition result.push([...path]); return; } for (const choice of choices) { path.push(choice); // choose backtrack(path, choices, result); // explore path.pop(); // undo } } ``` ## TypeScript ```typescript function backtrack(path: number[], choices: number[], result: number[][]): void { if (path.length === choices.length) { // or another stop condition result.push([...path]); return; } for (const choice of choices) { path.push(choice); // choose backtrack(path, choices, result); // explore path.pop(); // undo } } ``` ## Python ```python def backtrack(path, choices, result): if len(path) == len(choices): # or another stop condition result.append(path[:]) return for choice in choices: path.append(choice) # choose backtrack(path, choices, result) # explore path.pop() # undo ``` ## PHP ```php function backtrack(array &$path, array $choices, array &$result): void { if (count($path) === count($choices)) { $result[] = $path; return; } foreach ($choices as $choice) { $path[] = $choice; // choose backtrack($path, $choices, $result); // explore array_pop($path); // undo } } ``` ## Java ```java public static void backtrack(List path, int[] choices, List> result) { if (path.size() == choices.length) { // or another stop condition result.add(new ArrayList<>(path)); return; } for (int choice : choices) { path.add(choice); // choose backtrack(path, choices, result); // explore path.remove(path.size() - 1); // undo } } ``` ## C# ```csharp public static void Backtrack(List path, int[] choices, List> result) { if (path.Count == choices.Length) { // or another stop condition result.Add(new List(path)); return; } foreach (int choice in choices) { path.Add(choice); // choose Backtrack(path, choices, result); // explore path.RemoveAt(path.Count - 1); // undo } } ``` --- title: "Обход дерева" url: https://algopath.pro/ru/patterns/tree-traversal language: ru summary: "Обойдите каждый узел дерева ровно один раз. Именно то, в каком месте вы посещаете узел, и решает, что в итоге вычислит обход." updated: 2026-10-17 --- # Обход дерева Обойдите каждый узел дерева ровно один раз. Именно то, в каком месте вы посещаете узел, и решает, что в итоге вычислит обход. ## Обход дерева: как это работает? Любой обход делает в узле одно и то же. Он посещает узел и спускается в каждого потомка. Прямой обход посещает узел до потомков. Он нужен, когда значение родителя требуется первым. Симметричный обход идёт влево, потом узел, потом вправо. На дереве поиска это даёт отсортированный порядок. Обратный обход посещает обоих потомков до узла. Он нужен, когда ответ зависит от потомков. Обход в ширину использует очередь вместо рекурсии. Он работает рядами, а не вглубь. Меняется только место посещения. Сам проход во всех вариантах одинаков. Источники: [Википедия: Обход дерева](https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%85%D0%BE%D0%B4_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%B0) - `корень 1, потомки 2 и 3` Корень достигается первым при любом порядке. - `прямой: 1, 2, 3` Сначала записывается узел, потом оба потомка. - `симметричный: 2, 1, 3` Левый потомок записывается раньше самого узла. - `обратный: 2, 3, 1` Оба потомка завершаются до записи узла. - `по уровням: 1, 2, 3` Очередь читает ряд за рядом, а не вглубь. ## Обход дерева: когда применять? Паттерн «Обход дерева» нужен, когда в условии есть слова «бинарное дерево задано как корневой узел с указателями left/right», «посетить каждый узел (прямой / симметричный / обратный / по уровням)» или «посчитать глубину, высоту или сумму по всему дереву». Решение на нём обычно укладывается в O(n). - бинарное дерево задано как корневой узел с указателями left/right - посетить каждый узел (прямой / симметричный / обратный / по уровням) - посчитать глубину, высоту или сумму по всему дереву - обработать детей до или после родителя - вывод по уровням (BFS с очередью) ## Обход дерева: с чем путают? Паттерн «Обход дерева» чаще всего путают с этими паттернами: Обход графа BFS / DFS, Рекурсия, Стек (LIFO) и Двоичное дерево поиска. - **Обход графа BFS / DFS** - В граф можно попасть в узел дважды, поэтому нужно множество посещённых. В дереве нельзя, поэтому не нужно. - **Рекурсия** - Рекурсия это то, чем обход обычно записывают. Эта страница про три порядка посещения. - **Стек (LIFO)** - Итеративный обход меняет стек вызовов на свой. Порядок посещения при этом тот же. - **Двоичное дерево поиска** - В дереве поиска симметричный обход даёт отсортированные значения. В любом другом дереве этот порядок ничего не значит. ## Обход дерева: сложность по времени и памяти У паттерна «Обход дерева» целевая сложность O(n). n узлов дают O(n) времени. Память O(h) на стек, а h равно n на вырожденной цепочке. ## Обход дерева: разбор примера ### Диаметр двоичного дерева Верните самый длинный путь между любыми двумя узлами, считая в рёбрах. Путь не обязан проходить через корень. В любом узле лучший локальный путь это глубина слева плюс глубина справа. Значит считайте глубину обратным обходом. Лучшую сумму запоминайте при возврате. ```javascript function diameterOfBinaryTree(root) { let best = 0; function depth(node) { if (!node) return 0; const left = depth(node.left); const right = depth(node.right); // the path passing through this node, counted in edges best = Math.max(best, left + right); return 1 + Math.max(left, right); } depth(root); return best; } ``` ## Обход дерева: частые ошибки - **Нет проверки на пустой узел** Любая рекурсия обязана остановиться на пустом потомке. Эта проверка и есть базовый случай. - **Ставят посещение не туда** Строка посещения это вся разница между порядками. Её перенос меняет ответ. - **Кладут потомков в неверном порядке** Итеративный прямой обход кладёт сначала правого потомка. Стек переворачивает всё, что в него положили. - **Спускаются рекурсией по очень глубокому дереву** Миллион узлов в одной цепочке переполняет стек вызовов. Там нужен явный стек. ## Обход дерева: задачи с собеседований Паттерн «Обход дерева» решает такие задачи с собеседований: Симметричный обход дерева, Глубина двоичного дерева, Диаметр двоичного дерева, Обход дерева по уровням, Сумма на пути, Зеркальное отражение дерева и Вид дерева справа. - **Симметричный обход дерева** Чистая форма, рекурсией или через стек. - **Глубина двоичного дерева** Единица плюс более глубокий из двух потомков. - **Диаметр двоичного дерева** Глубина слева плюс глубина справа в каждом узле. - **Обход дерева по уровням** Очередь, по одному ряду за раунд. - **Сумма на пути** Несите накопленную сумму вниз по дереву. - **Зеркальное отражение дерева** Поменяйте местами двух потомков в каждом узле. - **Вид дерева справа** Последний узел, достигнутый на каждом уровне. ## JavaScript ```javascript function traverse(node, result = []) { if (!node) return result; // base case traverse(node.left, result); result.push(node.val); // inorder here; move this line for pre/post traverse(node.right, result); return result; } ``` ## TypeScript ```typescript interface TreeNode { val: number; left: TreeNode | null; right: TreeNode | null; } function traverse(node: TreeNode | null, result: number[] = []): number[] { if (!node) return result; // base case traverse(node.left, result); result.push(node.val); // inorder here; move this line for pre/post traverse(node.right, result); return result; } ``` ## Python ```python def traverse(node, result=None): if result is None: result = [] if not node: return result traverse(node.left, result) result.append(node.val) # inorder here; move this line for pre/post traverse(node.right, result) return result ``` ## PHP ```php function traverse(?TreeNode $node, array &$result = []): array { if ($node === null) return $result; traverse($node->left, $result); $result[] = $node->val; // inorder here; move this line for pre/post traverse($node->right, $result); return $result; } ``` ## Java ```java public static List traverse(TreeNode node, List result) { if (node == null) return result; // base case traverse(node.left, result); result.add(node.val); // inorder here; move this line for pre/post traverse(node.right, result); return result; } public static class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` ## C# ```csharp public static List Traverse(TreeNode node, List result) { if (node == null) return result; // base case Traverse(node.left, result); result.Add(node.val); // inorder here; move this line for pre/post Traverse(node.right, result); return result; } public class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` --- title: "Двоичное дерево поиска" url: https://algopath.pro/ru/patterns/bst language: ru summary: "Дерево поиска держит все меньшие значения слева, а большие справа. Каждый поиск отбрасывает целиком одну сторону дерева." updated: 2026-10-17 --- # Двоичное дерево поиска Дерево поиска держит все меньшие значения слева, а большие справа. Каждый поиск отбрасывает целиком одну сторону дерева. ## Двоичное дерево поиска: как это работает? Каждый узел делит своё поддерево надвое. Слева всё меньше, справа всё больше. Поиск сравнивает цель с текущим узлом. Сравнение выбирает одного потомка и отбрасывает другого. Вставка идёт тем же путём вниз. Новый узел становится потомком последнего достигнутого. Правило касается целых поддеревьев, а не только прямых потомков. Левый внук всё равно обязан быть меньше корня. Симметричный обход читает значения по возрастанию. Именно на это свойство опирается большинство задач. Отсортированный вход строит одну длинную цепочку. Тогда операция стоит n, и лечит это балансировка. Источники: [Википедия: Двоичное дерево поиска](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0) - `корень = 5` Вставляем 5, 3, 8 и 4. Первое значение становится корнем. - `3 уходит влево` Тройка меньше пятёрки. Она становится левым потомком. - `8 уходит вправо` Восьмёрка больше пятёрки. Она становится правым потомком. - `4 идёт влево, потом вправо` Четвёрка меньше пятёрки, но больше тройки. - `симметричный обход: 3, 4, 5, 8` Чтение по порядку даёт отсортированные значения. ## Двоичное дерево поиска: когда применять? Паттерн «Двоичное дерево поиска» нужен, когда в условии есть слова «двоичное дерево поиска: left < node < right в каждом узле», «вставка/удаление/поиск с сохранением порядка данных» или «проверить BST / k-й наименьший / симметричный обход даёт отсортированную последовательность». Решение на нём обычно укладывается в O(log n) avg. - двоичное дерево поиска: left < node < right в каждом узле - вставка/удаление/поиск с сохранением порядка данных - проверить BST / k-й наименьший / симметричный обход даёт отсортированную последовательность - ближайшее значение или запрос по диапазону на множестве, которое меняется со временем - массив заранее не дан, значения приходят по одному ## Двоичное дерево поиска: с чем путают? Паттерн «Двоичное дерево поиска» чаще всего путают с этими паттернами: Бинарный поиск (массив), Хеш-множество / словарь, Бинарная куча / очередь с приоритетом и Обход дерева. - **Бинарный поиск (массив)** - Массив делится пополам по индексу, но вставка в него дорога. Дерево делится по указателю и вставку берёт. - **Хеш-множество / словарь** - Словарь быстрее для точного поиска. Дерево ещё отвечает про диапазоны и даёт порядок. - **Бинарная куча / очередь с приоритетом** - Куча обещает только минимум в корне. Дерево поиска упорядочивает каждую пару узлов. - **Обход дерева** - Та страница про обход всего дерева. Эта про правило упорядочивания. ## Двоичное дерево поиска: сложность по времени и памяти У паттерна «Двоичное дерево поиска» целевая сложность O(log n) avg. Сбалансированное дерево при n до 1e6 даёт O(log n) на операцию. Несбалансированное вырождается в O(n). ## Двоичное дерево поиска: разбор примера ### Проверить, что дерево является деревом поиска Определите, соблюдает ли двоичное дерево правило дерева поиска. Все значения слева обязаны быть меньше, а все значения справа больше. Сравнения узла с двумя его потомками недостаточно. Вместо этого несите вниз нижнюю и верхнюю границу. Каждый шаг ужимает одну из них. ```javascript function isValidBST(root, low = -Infinity, high = Infinity) { if (!root) return true; // the bound comes from an ancestor, not from the parent alone if (root.val <= low || root.val >= high) return false; return isValidBST(root.left, low, root.val) && isValidBST(root.right, root.val, high); } ``` ## Двоичное дерево поиска: частые ошибки - **Проверяют только прямых потомков** Глубокий узел может нарушить правило относительно дальнего предка. Передавайте границы вниз. - **Считают дерево сбалансированным** Отсортированные вставки строят цепочку, а не дерево. Тогда log n на деле равно n. - **Не решают, куда идут дубликаты** Условие обязано сказать, в какую сторону идёт повтор. Выберите сторону и держитесь её. - **Небрежно удаляют узел с двумя потомками** Узел с двумя потомками заменяют следующим по порядку узлом. Любой другой ломает упорядоченность. ## Двоичное дерево поиска: задачи с собеседований Паттерн «Двоичное дерево поиска» решает такие задачи с собеседований: Проверка дерева поиска, Поиск в дереве поиска, Вставка в дерево поиска, Удаление узла из дерева поиска, K-е наименьшее в дереве поиска, Наименьший общий предок в дереве поиска и Дерево поиска из отсортированного массива. - **Проверка дерева поиска** Несите вниз нижнюю и верхнюю границу. - **Поиск в дереве поиска** Одно сравнение отбрасывает половину дерева. - **Вставка в дерево поиска** Спуститесь вниз и подвесьте узел к последнему. - **Удаление узла из дерева поиска** Заменяйте его следующим по порядку узлом. - **K-е наименьшее в дереве поиска** Прервите симметричный обход после k узлов. - **Наименьший общий предок в дереве поиска** Первый узел, оказавшийся между двумя значениями. - **Дерево поиска из отсортированного массива** Средний элемент становится корнем, дальше рекурсия. ## JavaScript ```javascript function insert(node, val) { if (!node) return { val, left: null, right: null }; if (val < node.val) node.left = insert(node.left, val); else if (val > node.val) node.right = insert(node.right, val); return node; } ``` ## TypeScript ```typescript interface TreeNode { val: number; left: TreeNode | null; right: TreeNode | null; } function insert(node: TreeNode | null, val: number): TreeNode { if (!node) return { val, left: null, right: null }; if (val < node.val) node.left = insert(node.left, val); else if (val > node.val) node.right = insert(node.right, val); return node; } ``` ## Python ```python def insert(node, val): if not node: return TreeNode(val) if val < node.val: node.left = insert(node.left, val) elif val > node.val: node.right = insert(node.right, val) return node ``` ## PHP ```php function insert(?TreeNode $node, int $val): TreeNode { if ($node === null) return new TreeNode($val); if ($val < $node->val) $node->left = insert($node->left, $val); elseif ($val > $node->val) $node->right = insert($node->right, $val); return $node; } ``` ## Java ```java public static TreeNode insert(TreeNode node, int val) { if (node == null) { TreeNode fresh = new TreeNode(); fresh.val = val; return fresh; } if (val < node.val) node.left = insert(node.left, val); else if (val > node.val) node.right = insert(node.right, val); return node; } public static class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` ## C# ```csharp public static TreeNode Insert(TreeNode node, int val) { if (node == null) return new TreeNode { val = val }; if (val < node.val) node.left = Insert(node.left, val); else if (val > node.val) node.right = Insert(node.right, val); return node; } public class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` --- title: "Наименьший общий предок" url: https://algopath.pro/ru/patterns/tree-lca language: ru summary: "Наименьший общий предок это самый глубокий узел, под которым лежат обе цели. Найти его хватает одного обратного обхода дерева." updated: 2026-10-17 --- # Наименьший общий предок Наименьший общий предок это самый глубокий узел, под которым лежат обе цели. Найти его хватает одного обратного обхода дерева. ## Наименьший общий предок: как это работает? Задайте каждому узлу один и тот же вопрос. Лежит ли в моём поддереве хоть одна цель? Пустой узел отвечает нет. Узел, который сам является целью, отвечает собой. Иначе спросите обоих потомков. Каждый вернёт узел или ничего. Если ответили оба потомка, предок это текущий узел. Цели расходятся именно здесь. Если ответил только один, передайте его ответ наверх. Расхождение ещё не случилось. Первый узел, где ответили обе стороны, и есть самый глубокий. Ничто выше него не может быть ниже. Источники: [Википедия: Наименьший общий предок](https://ru.wikipedia.org/wiki/%D0%9D%D0%B0%D0%B8%D0%BC%D0%B5%D0%BD%D1%8C%D1%88%D0%B8%D0%B9_%D0%BE%D0%B1%D1%89%D0%B8%D0%B9_%D0%BF%D1%80%D0%B5%D0%B4%D0%BE%D0%BA) - `узел 6: найдено` Ищем 6 и 2. Узел 6 отвечает собой. - `узел 2: найдено` Узел 2 тоже отвечает собой. - `узел 5: ответили обе стороны` Цели расходятся здесь. Ответ это узел 5. - `узел 3: ответила только левая` Правое поддерево ничего не нашло. Наверх идёт узел 5. - `ответ = 5` Один проход по дереву. Ни один узел не посещён дважды. ## Наименьший общий предок: когда применять? Паттерн «Наименьший общий предок» нужен, когда в условии есть слова «наименьший общий предок двух узлов», «самый глубокий узел, который является предком и p, и q» или «каждый рекурсивный вызов возвращает, какой из целевых узлов найден под ним». Решение на нём обычно укладывается в O(n). - наименьший общий предок двух узлов - самый глубокий узел, который является предком и p, и q - каждый рекурсивный вызов возвращает, какой из целевых узлов найден под ним - путь от корня до каждого узла и их сравнение (альтернативный подход) - дано бинарное дерево или BST, найти точку, где расходятся два пути ## Наименьший общий предок: с чем путают? Паттерн «Наименьший общий предок» чаще всего путают с этими паттернами: Двоичное дерево поиска, Обход дерева, Система непересекающихся множеств и DP на деревьях. - **Двоичное дерево поиска** - В дереве поиска направление задают сами значения. В обычном дереве надо спросить оба поддерева. - **Обход дерева** - Та страница про три порядка посещения. Здесь обратный обход отвечает на один вопрос. - **Система непересекающихся множеств** - Офлайновый алгоритм Тарьяна отвечает на много пар через непересекающиеся множества. Одной паре это не нужно. - **DP на деревьях** - Динамика на деревьях поднимает наверх посчитанные значения. Здесь наверх идёт только найдено или нет. ## Наименьший общий предок: сложность по времени и памяти У паттерна «Наименьший общий предок» целевая сложность O(n). n узлов дают O(n) на один запрос. Для многих запросов берут двоичные подъёмы по O(log n). ## Наименьший общий предок: разбор примера ### Предок внутри дерева поиска Оба узла лежат в двоичном дереве поиска. Найдите их наименьшего общего предка. Узел считается предком самого себя. Направление подсказывают сами значения. Если обе цели меньше, идите влево; если обе больше, вправо. Первый узел между ними и есть ответ. ```javascript function lowestCommonAncestor(root, p, q) { let node = root; while (node) { if (p.val < node.val && q.val < node.val) { node = node.left; } else if (p.val > node.val && q.val > node.val) { node = node.right; } else { return node; // the values split here, or one of them is this node } } return null; } ``` ## Наименьший общий предок: частые ошибки - **Ищут каждый узел по отдельности** Два поиска и сравнение путей это заметно больше работы. Один обратный обход отвечает сразу. - **Забывают, что узел бывает предком себе** Если одна цель лежит над другой, она и есть ответ. Возвращайте узел сразу при совпадении. - **Считают, что оба узла точно есть** Если одного нет, проход вернёт второй. Проверяйте наличие, когда отсутствие допустимо. - **Берут приём дерева поиска на обычном дереве** Сравнение значений работает только при гарантированном порядке. Иначе надо спрашивать оба поддерева. ## Наименьший общий предок: задачи с собеседований Паттерн «Наименьший общий предок» решает такие задачи с собеседований: Наименьший общий предок в двоичном дереве, Наименьший общий предок в дереве поиска, Предок при наличии ссылок на родителя, Предок самых глубоких листьев, Расстояние между двумя узлами, Наименьшее поддерево со всеми глубокими узлами и Пошаговый маршрут между узлами. - **Наименьший общий предок в двоичном дереве** Чистая форма: один обратный обход. - **Наименьший общий предок в дереве поиска** Направление задают одни только значения. - **Предок при наличии ссылок на родителя** Поднимайтесь от обоих узлов и встретьтесь. - **Предок самых глубоких листьев** Несите наверх глубину вместе с узлом. - **Расстояние между двумя узлами** Обе глубины минус удвоенная глубина предка. - **Наименьшее поддерево со всеми глубокими узлами** Та же форма, только цели другие. - **Пошаговый маршрут между узлами** Найдите предка, потом постройте оба пути. ## JavaScript ```javascript function lca(node, p, q) { if (!node || node === p || node === q) return node; const left = lca(node.left, p, q); const right = lca(node.right, p, q); if (left && right) return node; // p and q split here return left || right; } ``` ## TypeScript ```typescript interface TreeNode { val: number; left: TreeNode | null; right: TreeNode | null; } function lca(node: TreeNode | null, p: TreeNode, q: TreeNode): TreeNode | null { if (!node || node === p || node === q) return node; const left = lca(node.left, p, q); const right = lca(node.right, p, q); if (left && right) return node; // p and q split here return left ?? right; } ``` ## Python ```python def lca(node, p, q): if not node or node is p or node is q: return node left = lca(node.left, p, q) right = lca(node.right, p, q) if left and right: return node # p and q split here return left or right ``` ## PHP ```php function lca(?TreeNode $node, TreeNode $p, TreeNode $q): ?TreeNode { if ($node === null || $node === $p || $node === $q) return $node; $left = lca($node->left, $p, $q); $right = lca($node->right, $p, $q); if ($left !== null && $right !== null) return $node; // split point return $left ?? $right; } ``` ## Java ```java public static TreeNode lca(TreeNode node, TreeNode p, TreeNode q) { if (node == null || node == p || node == q) return node; TreeNode left = lca(node.left, p, q); TreeNode right = lca(node.right, p, q); if (left != null && right != null) return node; // p and q split here return left != null ? left : right; } public static class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` ## C# ```csharp public static TreeNode Lca(TreeNode node, TreeNode p, TreeNode q) { if (node == null || node == p || node == q) return node; TreeNode left = Lca(node.left, p, q); TreeNode right = Lca(node.right, p, q); if (left != null && right != null) return node; // p and q split here return left ?? right; } public class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` --- title: "Сериализация / десериализация дерева" url: https://algopath.pro/ru/patterns/tree-serialize language: ru summary: "Запишите дерево в одну плоскую строку и восстановите его точно таким же. Работает это за счёт меток отсутствующих потомков." updated: 2026-10-17 --- # Сериализация / десериализация дерева Запишите дерево в одну плоскую строку и восстановите его точно таким же. Работает это за счёт меток отсутствующих потомков. ## Сериализация / десериализация дерева: как это работает? Выберите один порядок обхода и держитесь его. Из прямого обхода восстанавливать проще всего. Записывайте значение узла в момент посещения. Разделяйте значения разделителем. Записывайте метку и для каждого отсутствующего потомка. Без этих меток форма теряется. При восстановлении читайте токены в том же порядке. Каждый токен даёт ровно один узел. Метка означает пустого потомка, значит сразу возврат. Значение создаёт узел и читает двух его потомков. Обе половины разбирают токены в одном порядке. Именно поэтому форма возвращается точно. Источники: [Википедия: Сериализация](https://ru.wikipedia.org/wiki/%D0%A1%D0%B5%D1%80%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F) - `пишем 1` Дерево это 1 с левым потомком 2. Прямой обход пишет узел первым. - `пишем 2` Следующим записывается левый потомок. - `пишем #, #` У узла 2 потомков нет. Это записывают две метки. - `пишем #` У узла 1 нет и правого потомка. - `1,2,#,#,#` Пять токенов на два узла. Форму несут именно метки. ## Сериализация / десериализация дерева: когда применять? Паттерн «Сериализация / десериализация дерева» нужен, когда в условии есть слова «сериализовать дерево в строку и десериализовать обратно», «восстановить точно ту же форму, а не только те же значения» или «формулировка encode/decode, разработать кодек для дерева». Решение на нём обычно укладывается в O(n). - сериализовать дерево в строку и десериализовать обратно - восстановить точно ту же форму, а не только те же значения - формулировка encode/decode, разработать кодек для дерева - маркеры null/None для отсутствующих детей - сохранить или передать дерево (по сети, в файл, в кеш) ## Сериализация / десериализация дерева: с чем путают? Паттерн «Сериализация / десериализация дерева» чаще всего путают с этими паттернами: Обход дерева, Рекурсия, Обход графа BFS / DFS и Двоичное дерево поиска. - **Обход дерева** - Обход только читает дерево. Сериализация ещё и записывает, где потомков нет. - **Рекурсия** - Обе половины это обычная рекурсия. Вся работа в том, чтобы договориться о формате. - **Обход графа BFS / DFS** - Формат по уровням не хуже, и его берут большинство сайтов с задачами. Читатель обязан совпасть с писателем. - **Двоичное дерево поиска** - Дереву поиска метки не нужны, форму задаёт порядок. Обычному дереву они необходимы. ## Сериализация / десериализация дерева: сложность по времени и памяти У паттерна «Сериализация / десериализация дерева» целевая сложность O(n). n узлов дают O(n) времени и O(n) на выход. Каждый узел и каждый отсутствующий потомок пишутся один раз. ## Сериализация / десериализация дерева: разбор примера ### Сериализовать и восстановить двоичное дерево Превратите двоичное дерево в строку и верните из строки то же самое дерево. Восстановленное дерево обязано совпасть с исходным узел в узел. Пишите прямым обходом, ставя решётку на каждого пустого потомка. Читайте обратно курсором по токенам. Решётка даёт null, всё остальное создаёт узел. ```javascript function serialize(root) { const out = []; (function write(node) { if (!node) { out.push("#"); // the marker is what preserves the shape return; } out.push(String(node.val)); write(node.left); write(node.right); })(root); return out.join(","); } function deserialize(data) { const tokens = data.split(","); let i = 0; function read() { const token = tokens[i++]; if (token === "#") return null; const node = new TreeNode(Number(token)); node.left = read(); // left must be consumed before right node.right = read(); return node; } return read(); } ``` ## Сериализация / десериализация дерева: частые ошибки - **Не пишут метки** Один прямой обход не скажет, где потомка не было. Тогда два разных дерева дают одну строку. - **Берут разделитель, встречающийся в данных** Значения бывают отрицательными и многозначными. Выберите символ, которого в значениях быть не может. - **Восстанавливают по арифметике индексов** Арифметика индексов работает только на полном дереве. Используйте курсор, идущий по токенам. - **Читают потомков в неверном порядке** Читатель обязан разобрать левого раньше правого. Некоторые языки вычисляют аргументы справа налево. ## Сериализация / десериализация дерева: задачи с собеседований Паттерн «Сериализация / десериализация дерева» решает такие задачи с собеседований: Сериализация двоичного дерева, Сериализация дерева поиска, Сериализация n-арного дерева, Дерево по прямому и симметричному обходу, Дерево по прямому и обратному обходу, Поиск одинаковых поддеревьев и Кодирование и декодирование строк. - **Сериализация двоичного дерева** Чистая форма, метка на каждого пустого потомка. - **Сериализация дерева поиска** Метки не нужны, форму задаёт порядок значений. - **Сериализация n-арного дерева** Пишите число потомков рядом с каждым значением. - **Дерево по прямому и симметричному обходу** Два обхода задают форму без всяких меток. - **Дерево по прямому и обратному обходу** Та же идея, но с одним неоднозначным случаем. - **Поиск одинаковых поддеревьев** Сериализуйте каждое поддерево и посчитайте строки. - **Кодирование и декодирование строк** Та же идея с префиксом длины, только для текста. ## JavaScript ```javascript function serialize(node) { if (!node) return "#"; return `${node.val},${serialize(node.left)},${serialize(node.right)}`; } function deserialize(data) { const vals = data.split(","); let i = 0; function build() { if (vals[i] === "#") { i++; return null; } const node = { val: Number(vals[i++]), left: null, right: null }; node.left = build(); node.right = build(); return node; } return build(); } ``` ## TypeScript ```typescript interface TreeNode { val: number; left: TreeNode | null; right: TreeNode | null; } function serialize(node: TreeNode | null): string { if (!node) return "#"; return `${node.val},${serialize(node.left)},${serialize(node.right)}`; } function deserialize(data: string): TreeNode | null { const vals = data.split(","); let i = 0; function build(): TreeNode | null { if (vals[i] === "#") { i++; return null; } const node: TreeNode = { val: Number(vals[i++]), left: null, right: null }; node.left = build(); node.right = build(); return node; } return build(); } ``` ## Python ```python def serialize(node): if not node: return "#" return f"{node.val},{serialize(node.left)},{serialize(node.right)}" def deserialize(data): vals = iter(data.split(",")) def build(): val = next(vals) if val == "#": return None node = TreeNode(int(val)) node.left = build() node.right = build() return node return build() ``` ## PHP ```php function serialize(?TreeNode $node): string { if ($node === null) return "#"; return $node->val . "," . serialize($node->left) . "," . serialize($node->right); } function deserialize(array &$vals): ?TreeNode { $val = array_shift($vals); if ($val === "#") return null; $node = new TreeNode((int) $val); $node->left = deserialize($vals); $node->right = deserialize($vals); return $node; } ``` ## Java ```java public static String serialize(TreeNode node) { if (node == null) return "#"; return node.val + "," + serialize(node.left) + "," + serialize(node.right); } public static TreeNode deserialize(String data) { return build(new ArrayDeque<>(Arrays.asList(data.split(",")))); } private static TreeNode build(Deque vals) { String val = vals.poll(); if (val == null || val.equals("#")) return null; TreeNode node = new TreeNode(); node.val = Integer.parseInt(val); node.left = build(vals); node.right = build(vals); return node; } public static class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` ## C# ```csharp public static string Serialize(TreeNode node) { if (node == null) return "#"; return node.val + "," + Serialize(node.left) + "," + Serialize(node.right); } public static TreeNode Deserialize(string data) { return Build(new Queue(data.Split(','))); } private static TreeNode Build(Queue vals) { string val = vals.Dequeue(); if (val == "#") return null; var node = new TreeNode(); node.val = int.Parse(val); node.left = Build(vals); node.right = Build(vals); return node; } public class TreeNode { public int val; public TreeNode left; public TreeNode right; } ``` --- title: "Бинарная куча / очередь с приоритетом" url: https://algopath.pro/ru/patterns/binary-heap language: ru summary: "Куча держит наверху наименьшее значение, а больше ничего не упорядочивает вообще. Вставка и снятие стоят по log n каждая." updated: 2026-10-17 --- # Бинарная куча / очередь с приоритетом Куча держит наверху наименьшее значение, а больше ничего не упорядочивает вообще. Вставка и снятие стоят по log n каждая. ## Бинарная куча / очередь с приоритетом: как это работает? Куча это дерево, лежащее в плоском массиве. Потомки индекса i стоят на 2i плюс один и два. Единственное правило: родитель сильнее своих потомков. Между собой соседи не упорядочены никак. Вставка пишет в конец, потом поднимает значение вверх. Оно меняется с родителем, пока сильнее него. Снятие забирает корень, переносит туда последний элемент и опускает его. Он меняется с лучшим потомком, пока не встанет. Оба действия идут по одному пути дерева. Высота равна log n, значит и стоимость тоже. Смысл имеет только корень. Любая другая позиция произвольна. Источники: [Википедия: Двоичная куча](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%B0%D1%8F_%D0%BA%D1%83%D1%87%D0%B0), [Документация Python (англ.): heapq](https://docs.python.org/3/library/heapq.html) - `[5]` Кладём 5, 3 и 8. Первое значение становится корнем. - `[3, 5]` Тройка кладётся и поднимается выше пятёрки. - `[3, 5, 8]` Восьмёрка остаётся там, куда легла. Родителя она не бьёт. - `снятие возвращает 3` Корень забирается. Наверх переезжает последний элемент, восьмёрка. - `[5, 8]` Восьмёрка опускается под пятёрку. Новый корень это минимум остатка. ## Бинарная куча / очередь с приоритетом: когда применять? Паттерн «Бинарная куча / очередь с приоритетом» нужен, когда в условии есть слова «всегда нужен текущий минимум/максимум», «k наименьших/наибольших элементов» или «слияние k отсортированных списков». Решение на нём обычно укладывается в O(log n) per op. - всегда нужен текущий минимум/максимум - k наименьших/наибольших элементов - слияние k отсортированных списков - текущая медиана, пока элементы прибывают - очередь с приоритетом / самое срочное следующим ## Бинарная куча / очередь с приоритетом: с чем путают? Паттерн «Бинарная куча / очередь с приоритетом» чаще всего путают с этими паттернами: Быстрая сортировка (merge / quick), Двоичное дерево поиска, Монотонная дек-очередь (максимум/минимум окна) и Алгоритм Дейкстры. - **Быстрая сортировка (merge / quick)** - Сортировка один раз даёт полный порядок. Куча отдаёт голову много раз, пока данные ещё идут. - **Двоичное дерево поиска** - Дерево поиска упорядочивает каждую пару узлов. Куча обещает только корень. - **Монотонная дек-очередь (максимум/минимум окна)** - Дек умеет выбросить только что устаревший элемент. У кучи дешёвого способа для этого нет. - **Алгоритм Дейкстры** - Дейкстра это самый известный потребитель кучи. Эта страница про сам контейнер. ## Бинарная куча / очередь с приоритетом: сложность по времени и памяти У паттерна «Бинарная куча / очередь с приоритетом» целевая сложность O(log n) per op. n до 1e6 даёт O(log n) на вставку или снятие. Чтение вершины стоит O(1). ## Бинарная куча / очередь с приоритетом: разбор примера ### K ближайших точек к началу координат Верните k точек, лежащих ближе всего к началу координат. Список бывает слишком длинным для сортировки, а нужны только k точек. Держите кучу размера k, на вершине которой худшая из сохранённых точек. Кладите каждую точку и снимайте вершину, как только куча переросла k. Остаток и есть ответ. ```javascript function kClosest(points, k) { // a max-heap keyed by squared distance: the worst kept point sits on top const heap = new MaxHeap((p) => p[0] * p[0] + p[1] * p[1]); for (const point of points) { heap.push(point); if (heap.size() > k) { heap.pop(); // drops the farthest, never the closest } } return heap.toArray(); } ``` ## Бинарная куча / очередь с приоритетом: частые ошибки - **Ждут отсортированный массив** Упорядочен только корень. Чтение массива подряд даёт бессмыслицу. - **Держат все элементы ради вопроса про k** Куча размера k стоит O(n log k). Хранение всех тратит память впустую. - **Строят кучу не в ту сторону** Для k наибольших нужна min-куча, чтобы худший сохранённый был наверху. Это легко перепутать. - **Пытаются удалить произвольный элемент** У кучи нет дешёвого способа его найти. Пометьте его мёртвым и пропустите при всплытии. ## Бинарная куча / очередь с приоритетом: задачи с собеседований Паттерн «Бинарная куча / очередь с приоритетом» решает такие задачи с собеседований: K-й по величине в потоке, K ближайших точек к началу, Слияние k отсортированных списков, K самых частых элементов, Планировщик задач, Медиана потока чисел и Кратчайший путь Дейкстры. - **K-й по величине в потоке** Min-куча размера k, ответ лежит в корне. - **K ближайших точек к началу** Ограниченная max-куча по квадрату расстояния. - **Слияние k отсортированных списков** Куча держит головной узел каждого списка. - **K самых частых элементов** Сначала счётчики, потом куча размера k. - **Планировщик задач** Самая частая задача идёт первой в каждом раунде. - **Медиана потока чисел** Две кучи, смотрящие друг на друга через середину. - **Кратчайший путь Дейкстры** Куча решает, какую вершину закрывать следующей. ## JavaScript ```javascript function kSmallest(nums, k) { const heap = []; // max-heap: keeps the k smallest seen so far const swap = (i, j) => ([heap[i], heap[j]] = [heap[j], heap[i]]); function push(v) { heap.push(v); let i = heap.length - 1; while (i > 0 && heap[(i - 1) >> 1] < heap[i]) { swap((i - 1) >> 1, i); i = (i - 1) >> 1; } } function pop() { const top = heap[0]; heap[0] = heap.pop(); let i = 0; while (2 * i + 1 < heap.length) { let c = 2 * i + 1; if (c + 1 < heap.length && heap[c + 1] > heap[c]) c++; if (heap[i] >= heap[c]) break; swap(i, c); i = c; } return top; } for (const x of nums) { push(x); if (heap.length > k) pop(); } return heap.slice().sort((a, b) => a - b); } ``` ## TypeScript ```typescript function kSmallest(nums: number[], k: number): number[] { const heap: number[] = []; // max-heap: keeps the k smallest seen so far const swap = (i: number, j: number): void => { [heap[i], heap[j]] = [heap[j], heap[i]]; }; function push(v: number): void { heap.push(v); let i = heap.length - 1; while (i > 0 && heap[(i - 1) >> 1] < heap[i]) { swap((i - 1) >> 1, i); i = (i - 1) >> 1; } } function pop(): number { const top = heap[0]; heap[0] = heap.pop()!; let i = 0; while (2 * i + 1 < heap.length) { let c = 2 * i + 1; if (c + 1 < heap.length && heap[c + 1] > heap[c]) c++; if (heap[i] >= heap[c]) break; swap(i, c); i = c; } return top; } for (const x of nums) { push(x); if (heap.length > k) pop(); } return heap.slice().sort((a, b) => a - b); } ``` ## Python ```python import heapq def k_smallest(nums, k): heap = [] # max-heap via negation: keeps the k smallest seen so far for x in nums: heapq.heappush(heap, -x) if len(heap) > k: heapq.heappop(heap) return sorted(-x for x in heap) ``` ## PHP ```php function kSmallest(array $nums, int $k): array { $heap = new SplMaxHeap(); // keeps the k smallest seen so far foreach ($nums as $x) { $heap->insert($x); if ($heap->count() > $k) { $heap->extract(); } } $result = []; foreach ($heap as $x) { $result[] = $x; } sort($result); return $result; } ``` ## Java ```java public static List kSmallest(int[] nums, int k) { // max-heap: keeps the k smallest seen so far PriorityQueue heap = new PriorityQueue<>(Comparator.reverseOrder()); for (int x : nums) { heap.add(x); if (heap.size() > k) heap.poll(); } List result = new ArrayList<>(heap); Collections.sort(result); return result; } ``` ## C# ```csharp public static List KSmallest(int[] nums, int k) { // max-heap via a negated key: keeps the k smallest seen so far var heap = new PriorityQueue(); foreach (int x in nums) { heap.Enqueue(x, -x); if (heap.Count > k) heap.Dequeue(); } var result = new List(); while (heap.Count > 0) result.Add(heap.Dequeue()); result.Sort(); return result; } ``` --- title: "Обход графа BFS / DFS" url: https://algopath.pro/ru/patterns/graph-bfs-dfs language: ru summary: "Обходите граф от стартовой вершины, помечая всё уже увиденное. Очередь даёт кратчайший путь в рёбрах, а стек уходит вглубь." updated: 2026-10-17 --- # Обход графа BFS / DFS Обходите граф от стартовой вершины, помечая всё уже увиденное. Очередь даёт кратчайший путь в рёбрах, а стек уходит вглубь. ## Обход графа BFS / DFS: как это работает? Положите стартовую вершину в контейнер и пометьте её. Контейнером служит очередь или стек. Достаньте одну вершину. Посмотрите на всех её соседей. Пропускайте уже помеченных соседей. Именно эта проверка не даёт обходу зациклиться. Пометьте каждого нового соседа и положите в контейнер. Помечайте при вставке, а не при извлечении. Очередь берёт самую старую вершину, поэтому обход растекается по уровням. Так до каждой вершины выходит минимум рёбер. Стек берёт самую свежую, поэтому обход сначала уходит вглубь. Оба посещают каждую достижимую вершину один раз. Источники: [Википедия: Поиск в ширину](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%B8%D1%81%D0%BA_%D0%B2_%D1%88%D0%B8%D1%80%D0%B8%D0%BD%D1%83), [Википедия: Поиск в глубину](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%B8%D1%81%D0%BA_%D0%B2_%D0%B3%D0%BB%D1%83%D0%B1%D0%B8%D0%BD%D1%83) - `очередь = [1], помечены = {1}` Рёбра это 1-2, 1-3 и 2-4. Старт помечается первым. - `берём 1, очередь = [2, 3]` Оба соседа новые. Они помечаются при вставке. - `берём 2, очередь = [3, 4]` Вершина 4 новая. Вершина 1 уже помечена и пропускается. - `берём 3, очередь = [4]` У вершины 3 непомеченных соседей нет. - `берём 4, очередь = []` Ничего не осталось. Четыре вершины, каждая посещена один раз. ## Обход графа BFS / DFS: когда применять? Паттерн «Обход графа BFS / DFS» нужен, когда в условии есть слова «кратчайший путь в невзвешенном графе», «минимальное число шагов/рёбер между узлами» или «достижим ли узел Y из узла X». Решение на нём обычно укладывается в O(V+E). - кратчайший путь в невзвешенном графе - минимальное число шагов/рёбер между узлами - достижим ли узел Y из узла X - исследовать кольцо за кольцом / слой за слоем - граф задан списком смежности, возможны циклы ## Обход графа BFS / DFS: с чем путают? Паттерн «Обход графа BFS / DFS» чаще всего путают с этими паттернами: Обход дерева, Алгоритм Дейкстры, Компоненты связности и Бэктрекинг. - **Обход дерева** - В дереве в узел нельзя попасть дважды, поэтому множество посещённых не нужно. В графе можно, поэтому нужно всегда. - **Алгоритм Дейкстры** - Веса на рёбрах ломают порядок по уровням. Тогда очередь заменяется кучей. - **Компоненты связности** - Та страница считает группы в графе. Здесь описан сам обход, из которого счёт и складывается. - **Бэктрекинг** - Бэктрекинг откатывает состояние на выходе. Обход помечает узел и никогда не снимает пометку. ## Обход графа BFS / DFS: сложность по времени и памяти У паттерна «Обход графа BFS / DFS» целевая сложность O(V+E). V вершин и E рёбер дают O(V + E). Память O(V) на множество посещённых и на фронт. ## Обход графа BFS / DFS: разбор примера ### Через сколько минут сгниют все апельсины В сетке лежат пустые клетки, свежие апельсины и гнилые. Каждую минуту гнилой заражает четырёх соседей. Верните число минут до исчезновения свежих или минус один. Все гнилые апельсины попадают в очередь ещё до начала обхода. За минуту обрабатывается целый уровень очереди. Оставшийся свежий апельсин означает минус один. ```javascript function orangesRotting(grid) { const rows = grid.length; const cols = grid[0].length; let queue = []; let fresh = 0; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (grid[r][c] === 2) queue.push([r, c]); if (grid[r][c] === 1) fresh++; } } const steps = [[1, 0], [-1, 0], [0, 1], [0, -1]]; let minutes = 0; while (queue.length && fresh > 0) { const next = []; // one whole level per minute for (const [r, c] of queue) { for (const [dr, dc] of steps) { const nr = r + dr; const nc = c + dc; if (nr < 0 || nc < 0 || nr >= rows || nc >= cols) continue; if (grid[nr][nc] !== 1) continue; grid[nr][nc] = 2; // marked on entry, so it is queued once fresh--; next.push([nr, nc]); } } queue = next; minutes++; } return fresh === 0 ? minutes : -1; } ``` ## Обход графа BFS / DFS: частые ошибки - **Помечают при извлечении, а не при вставке** Тогда одна вершина попадает в очередь много раз. Помечайте её в момент вставки. - **Берут стек, когда ответ это расстояние** Обход в глубину может дойти до вершины окольным путём. Минимум рёбер даёт только очередь. - **Вызывают shift на длинной очереди** В JavaScript shift на массиве стоит O(n). Используйте индекс чтения или список уровня. - **Забывают, что граф бывает несвязным** Один обход достаёт только одну компоненту. Пройдите по всем ещё не посещённым вершинам. ## Обход графа BFS / DFS: задачи с собеседований Паттерн «Обход графа BFS / DFS» решает такие задачи с собеседований: Число островов, Гниющие апельсины, Лестница слов, Клонирование графа, Кратчайший путь в двоичной матрице, Расписание курсов и Сток воды в два океана. - **Число островов** По одному обходу на каждую непосещённую клетку суши. - **Гниющие апельсины** Все гнилые клетки стартуют в очереди вместе. - **Лестница слов** Слова это вершины, а замены одной буквы это рёбра. - **Клонирование графа** Словарь посещённых хранит ещё и созданные копии. - **Кратчайший путь в двоичной матрице** Восемь направлений, одна очередь, счёт по уровням. - **Расписание курсов** Поиск цикла в ориентированном графе. - **Сток воды в два океана** Два обхода, оба стартуют от краёв. ## JavaScript ```javascript function bfsDistances(adj, source) { const dist = { [source]: 0 }; const queue = [source]; while (queue.length) { const node = queue.shift(); for (const next of adj[node] || []) { if (!(next in dist)) { dist[next] = dist[node] + 1; queue.push(next); } } } return dist; } ``` ## TypeScript ```typescript function bfsDistances(adj: Record, source: string): Record { const dist: Record = { [source]: 0 }; const queue: string[] = [source]; while (queue.length) { const node = queue.shift()!; for (const next of adj[node] ?? []) { if (!(next in dist)) { dist[next] = dist[node] + 1; queue.push(next); } } } return dist; } ``` ## Python ```python from collections import deque def bfs_distances(adj, source): dist = {source: 0} queue = deque([source]) while queue: node = queue.popleft() for nxt in adj.get(node, []): if nxt not in dist: dist[nxt] = dist[node] + 1 queue.append(nxt) return dist ``` ## PHP ```php function bfsDistances(array $adj, $source): array { $dist = [$source => 0]; $queue = [$source]; while ($queue) { $node = array_shift($queue); foreach ($adj[$node] ?? [] as $next) { if (!isset($dist[$next])) { $dist[$next] = $dist[$node] + 1; $queue[] = $next; } } } return $dist; } ``` ## Java ```java public static Map bfsDistances(Map> adj, String source) { Map dist = new HashMap<>(); dist.put(source, 0); Queue queue = new ArrayDeque<>(); queue.add(source); while (!queue.isEmpty()) { String node = queue.poll(); for (String next : adj.getOrDefault(node, List.of())) { if (!dist.containsKey(next)) { dist.put(next, dist.get(node) + 1); queue.add(next); } } } return dist; } ``` ## C# ```csharp public static Dictionary BfsDistances(Dictionary> adj, string source) { var dist = new Dictionary { [source] = 0 }; var queue = new Queue(); queue.Enqueue(source); while (queue.Count > 0) { string node = queue.Dequeue(); if (!adj.TryGetValue(node, out var neighbours)) continue; foreach (string next in neighbours) { if (!dist.ContainsKey(next)) { dist[next] = dist[node] + 1; queue.Enqueue(next); } } } return dist; } ``` --- title: "Компоненты связности" url: https://algopath.pro/ru/patterns/graph-components language: ru summary: "Начинайте новый обход в каждой ещё не увиденной вершине. Каждый реально начатый обход означает ещё одну связную группу графа." updated: 2026-10-17 --- # Компоненты связности Начинайте новый обход в каждой ещё не увиденной вершине. Каждый реально начатый обход означает ещё одну связную группу графа. ## Компоненты связности: как это работает? Держите одно множество посещённых на весь запуск. Между обходами оно не сбрасывается. Пройдите по всем вершинам графа. Уже помеченные пропускайте. Непомеченная вершина открывает новую компоненту. Прибавьте единицу к счётчику. Запустите из неё полный обход. Всё, до чего он дошёл, входит в эту компоненту. Обход помечает всё, чего коснулся. Значит ничто из этого не начнёт обход позже. Когда цикл кончился, в счётчике лежит число групп. Каждая вершина посещена один раз. Источники: [Википедия: Компонента связности графа](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BC%D0%BF%D0%BE%D0%BD%D0%B5%D0%BD%D1%82%D0%B0_%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D0%BE%D1%81%D1%82%D0%B8_%D0%B3%D1%80%D0%B0%D1%84%D0%B0) - `счёт = 0, посещённые = {}` Шесть вершин, рёбра 0-1, 1-2 и 3-4. - `обход из 0` Вершина 0 не помечена, значит открылась компонента. Обход доходит до 1 и 2. - `счёт = 1, посещённые = {0, 1, 2}` Вершины 1 и 2 помечены, поэтому обход не начинают. - `обход из 3, счёт = 2` Вершина 3 не помечена. Её обход доходит до вершины 4. - `обход из 5, счёт = 3` У вершины 5 нет рёбер. Она сама себе компонента. ## Компоненты связности: когда применять? Паттерн «Компоненты связности» нужен, когда в условии есть слова «посчитать число отдельных групп/островов/кластеров», «граф может быть несвязным» или «сколько связных частей». Решение на нём обычно укладывается в O(V+E). - посчитать число отдельных групп/островов/кластеров - граф может быть несвязным - сколько связных частей - круги друзей - разметить связные области ## Компоненты связности: с чем путают? Паттерн «Компоненты связности» чаще всего путают с этими паттернами: Обход графа BFS / DFS, Система непересекающихся множеств, Проверка двудольности (раскраска в два цвета) и Топологическая сортировка (алгоритм Кана). - **Обход графа BFS / DFS** - Это сам обход. Здесь описан цикл, запускающий по одному обходу на группу. - **Система непересекающихся множеств** - Она отвечает, пока рёбра ещё приходят. Заливке нужен весь граф сразу. - **Проверка двудольности (раскраска в два цвета)** - Раскраска ведёт тот же обход, но несёт цвет. Она отвечает на другой вопрос. - **Топологическая сортировка (алгоритм Кана)** - Упорядочиванию нужны направленные рёбра без циклов. Компоненты считают любое ребро ненаправленным. ## Компоненты связности: сложность по времени и памяти У паттерна «Компоненты связности» целевая сложность O(V+E). V вершин и E рёбер дают в сумме O(V + E). Каждую вершину посещает ровно один обход. ## Компоненты связности: разбор примера ### Посчитать острова в сетке В сетке лежат клетки суши и воды. Посчитайте, сколько в ней отдельных островов. Клетки соприкасаются только сверху, снизу, слева и справа. Идите по сетке клетка за клеткой. Непомеченная клетка суши открывает новый остров. Залейте оттуда весь остров, помечая по пути. Число заливок и есть ответ. ```javascript function numIslands(grid) { let count = 0; function sink(r, c) { if (r < 0 || c < 0 || r >= grid.length || c >= grid[0].length) return; if (grid[r][c] !== "1") return; grid[r][c] = "0"; // marked in place, so no visited set is needed sink(r + 1, c); sink(r - 1, c); sink(r, c + 1); sink(r, c - 1); } for (let r = 0; r < grid.length; r++) { for (let c = 0; c < grid[0].length; c++) { if (grid[r][c] === "1") { count++; // one new island sink(r, c); } } } return count; } ``` ## Компоненты связности: частые ошибки - **Сбрасывают множество посещённых на каждый обход** Тогда одна компонента считается много раз. Одно множество живёт весь запуск. - **Считают вершины вместо обходов** Ответ это число начатых обходов. А не число вершин, которых каждый коснулся. - **Помечают после спуска** Тогда цикл вернёт обход назад раньше, чем пометка появится. Помечайте до спуска. - **Идут рекурсией по огромной сетке** Остров на миллион клеток переполняет стек вызовов. Возьмите явный стек или очередь. ## Компоненты связности: задачи с собеседований Паттерн «Компоненты связности» решает такие задачи с собеседований: Число островов, Число провинций, Компоненты связности неориентированного графа, Наибольшая площадь острова, Число подостровов, Создание большого острова и Лишнее ребро. - **Число островов** По одной заливке на каждую непомеченную клетку суши. - **Число провинций** Граф приходит матрицей смежности. - **Компоненты связности неориентированного графа** Чистая форма, по списку рёбер. - **Наибольшая площадь острова** Возвращается размер наибольшего обхода, а не их число. - **Число подостровов** Считать только если каждая клетка суша и во второй сетке. - **Создание большого острова** Пометьте острова, потом проверьте каждую клетку воды. - **Лишнее ребро** Здесь ответ даёт система непересекающихся множеств. ## JavaScript ```javascript function countComponents(n, adj) { const visited = new Set(); let count = 0; for (let start = 0; start < n; start++) { if (visited.has(start)) continue; count++; const stack = [start]; visited.add(start); while (stack.length) { const node = stack.pop(); for (const next of adj[node] || []) { if (!visited.has(next)) { visited.add(next); stack.push(next); } } } } return count; } ``` ## TypeScript ```typescript function countComponents(n: number, adj: number[][]): number { const visited = new Set(); let count = 0; for (let start = 0; start < n; start++) { if (visited.has(start)) continue; count++; const stack: number[] = [start]; visited.add(start); while (stack.length) { const node = stack.pop()!; for (const next of adj[node] ?? []) { if (!visited.has(next)) { visited.add(next); stack.push(next); } } } } return count; } ``` ## Python ```python def count_components(n, adj): visited = set() count = 0 for start in range(n): if start in visited: continue count += 1 stack = [start] visited.add(start) while stack: node = stack.pop() for nxt in adj.get(node, []): if nxt not in visited: visited.add(nxt) stack.append(nxt) return count ``` ## PHP ```php function countComponents(int $n, array $adj): int { $visited = []; $count = 0; for ($start = 0; $start < $n; $start++) { if (isset($visited[$start])) continue; $count++; $stack = [$start]; $visited[$start] = true; while ($stack) { $node = array_pop($stack); foreach ($adj[$node] ?? [] as $next) { if (!isset($visited[$next])) { $visited[$next] = true; $stack[] = $next; } } } } return $count; } ``` ## Java ```java public static int countComponents(int n, List> adj) { Set visited = new HashSet<>(); int count = 0; for (int start = 0; start < n; start++) { if (visited.contains(start)) continue; count++; Deque stack = new ArrayDeque<>(); stack.push(start); visited.add(start); while (!stack.isEmpty()) { int node = stack.pop(); for (int next : adj.get(node)) { if (visited.add(next)) stack.push(next); } } } return count; } ``` ## C# ```csharp public static int CountComponents(int n, List> adj) { var visited = new HashSet(); int count = 0; for (int start = 0; start < n; start++) { if (visited.Contains(start)) continue; count++; var stack = new Stack(); stack.Push(start); visited.Add(start); while (stack.Count > 0) { int node = stack.Pop(); foreach (int next in adj[node]) { if (visited.Add(next)) stack.Push(next); } } } return count; } ``` --- title: "Топологическая сортировка (алгоритм Кана)" url: https://algopath.pro/ru/patterns/topological-sort language: ru summary: "Расставьте задачи так, чтобы каждая шла после всех, от которых она зависит. Каждый раз берите вершину, которая никому не должна." updated: 2026-10-17 --- # Топологическая сортировка (алгоритм Кана) Расставьте задачи так, чтобы каждая шла после всех, от которых она зависит. Каждый раз берите вершину, которая никому не должна. ## Топологическая сортировка (алгоритм Кана): как это работает? Посчитайте, сколько рёбер входит в каждую вершину. Это число называют входящей степенью. Положите в очередь все вершины с нулевой входящей степенью. Они никому не должны и идут первыми. Достаньте вершину и допишите её в ответ. Теперь она расставлена окончательно. Пройдите по её исходящим рёбрам и уменьшите счётчик каждой цели. У цели стало на один долг меньше. Счётчик, дошедший до нуля, попадает в очередь. Все её зависимости уже расставлены. Если ответ короче числа вершин, в графе есть цикл. Вершины в цикле нуля не достигают. Источники: [Википедия: Топологическая сортировка](https://ru.wikipedia.org/wiki/%D0%A2%D0%BE%D0%BF%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0), [Kahn (1962), Topological sorting of large networks (англ.)](https://doi.org/10.1145/368996.369025) - `степени: A 0, B 1, C 1, D 2` Рёбра это A в B, A в C, B в D и C в D. - `очередь = [A], ответ = []` Никому не должна только A, она и есть точка старта. - `берём A, ответ = [A]` B и C теряют по одному долгу. Обе дошли до нуля. - `очередь = [B, C]` У D всё ещё два долга. Она ждёт. - `ответ = [A, B, C, D]` D входит, когда оба её долга закрыты. ## Топологическая сортировка (алгоритм Кана): когда применять? Паттерн «Топологическая сортировка (алгоритм Кана)» нужен, когда в условии есть слова «задача должна идти перед другой», «порядок, соблюдающий зависимости» или «обнаружить цикл в предпосылках». Решение на нём обычно укладывается в O(V+E). - задача должна идти перед другой - порядок, соблюдающий зависимости - обнаружить цикл в предпосылках - порядок сборки / расписание курсов / порядок установки - ориентированный граф, X должен произойти перед Y ## Топологическая сортировка (алгоритм Кана): с чем путают? Паттерн «Топологическая сортировка (алгоритм Кана)» чаще всего путают с этими паттернами: Обход графа BFS / DFS, Компоненты связности, Система непересекающихся множеств и Жадный алгоритм (обменный аргумент). - **Обход графа BFS / DFS** - Обычный обход посещает вершины в любом удобном порядке. Здесь порядок и есть ответ. - **Компоненты связности** - Компоненты не смотрят на направление и только группируют вершины. Здесь направление задаёт весь ответ. - **Система непересекающихся множеств** - Она считает любое ребро ненаправленным. Направление здесь и есть самое главное. - **Жадный алгоритм (обменный аргумент)** - Правило жадное: берите всё, у чего не осталось долгов. Удаление вершины не может добавить зависимость. ## Топологическая сортировка (алгоритм Кана): сложность по времени и памяти У паттерна «Топологическая сортировка (алгоритм Кана)» целевая сложность O(V+E). V вершин и E рёбер дают O(V + E). Каждое ребро убирается ровно один раз. ## Топологическая сортировка (алгоритм Кана): разбор примера ### Порядок, в котором можно пройти все курсы Дано число курсов и список пар с предварительными требованиями. Верните порядок, в котором можно пройти всё. Если такого порядка нет, верните пустой список. Постройте список смежности и входящие степени за один проход. Потом крутите цикл очереди. Ответ короче числа курсов означает цикл. ```javascript function findOrder(numCourses, prerequisites) { const next = Array.from({ length: numCourses }, () => []); const indegree = new Array(numCourses).fill(0); for (const [course, needs] of prerequisites) { next[needs].push(course); indegree[course]++; } const queue = []; for (let i = 0; i < numCourses; i++) { if (indegree[i] === 0) queue.push(i); } const order = []; for (let i = 0; i < queue.length; i++) { // index walk, never shift const node = queue[i]; order.push(node); for (const target of next[node]) { if (--indegree[target] === 0) queue.push(target); } } // a short answer means some nodes never reached zero return order.length === numCourses ? order : []; } ``` ## Топологическая сортировка (алгоритм Кана): частые ошибки - **Строят рёбра в обратную сторону** Пара читается как курс и требование к нему. Переворот молча решает совсем другую задачу. - **Не проверяют длину в конце** Цикл даёт короткий ответ, а не ошибку. Сравните длину с числом вершин. - **Ждут единственный правильный порядок** Готовых вершин в один момент бывает несколько. Любой порядок среди них верен. - **Берут shift на очереди** В JavaScript это O(n) на каждое удаление. Идите по массиву индексом. ## Топологическая сортировка (алгоритм Кана): задачи с собеседований Паттерн «Топологическая сортировка (алгоритм Кана)» решает такие задачи с собеседований: Расписание курсов, Расписание курсов II, Словарь инопланетян, Деревья минимальной высоты, Восстановление последовательности, Параллельные курсы и Сортировка элементов по группам. - **Расписание курсов** Нужно только знать, существует ли порядок. - **Расписание курсов II** Нужен сам порядок, а не ответ да или нет. - **Словарь инопланетян** Рёбра выводятся сравнением соседних слов. - **Деревья минимальной высоты** Здесь слои листьев снимают снаружи внутрь. - **Восстановление последовательности** Проверьте, что на каждом шаге готова ровно одна вершина. - **Параллельные курсы** Считаются раунды, а не выдаётся порядок. - **Сортировка элементов по группам** Два порядка: внутри групп и между ними. ## JavaScript ```javascript function topoSort(n, adj) { const indeg = new Array(n).fill(0); for (const u in adj) for (const v of adj[u]) indeg[v]++; const queue = []; for (let i = 0; i < n; i++) if (indeg[i] === 0) queue.push(i); const order = []; while (queue.length) { const u = queue.shift(); order.push(u); for (const v of adj[u] || []) { if (--indeg[v] === 0) queue.push(v); } } return order.length === n ? order : null; // null: a cycle exists } ``` ## TypeScript ```typescript function topoSort(n: number, adj: number[][]): number[] | null { const indeg = new Array(n).fill(0); for (const edges of adj) for (const v of edges) indeg[v]++; const queue: number[] = []; for (let i = 0; i < n; i++) if (indeg[i] === 0) queue.push(i); const order: number[] = []; while (queue.length) { const u = queue.shift()!; order.push(u); for (const v of adj[u] ?? []) { if (--indeg[v] === 0) queue.push(v); } } return order.length === n ? order : null; // null: a cycle exists } ``` ## Python ```python from collections import deque def topo_sort(n, adj): indeg = [0] * n for u in adj: for v in adj[u]: indeg[v] += 1 queue = deque(i for i in range(n) if indeg[i] == 0) order = [] while queue: u = queue.popleft() order.append(u) for v in adj.get(u, []): indeg[v] -= 1 if indeg[v] == 0: queue.append(v) return order if len(order) == n else None # None: a cycle exists ``` ## PHP ```php function topoSort(int $n, array $adj): ?array { $indeg = array_fill(0, $n, 0); foreach ($adj as $u => $neighbors) { foreach ($neighbors as $v) $indeg[$v]++; } $queue = []; for ($i = 0; $i < $n; $i++) if ($indeg[$i] === 0) $queue[] = $i; $order = []; while ($queue) { $u = array_shift($queue); $order[] = $u; foreach ($adj[$u] ?? [] as $v) { if (--$indeg[$v] === 0) $queue[] = $v; } } return count($order) === $n ? $order : null; // null: a cycle exists } ``` ## Java ```java public static List topoSort(int n, List> adj) { int[] indeg = new int[n]; for (List edges : adj) for (int v : edges) indeg[v]++; Queue queue = new ArrayDeque<>(); for (int i = 0; i < n; i++) if (indeg[i] == 0) queue.add(i); List order = new ArrayList<>(); while (!queue.isEmpty()) { int u = queue.poll(); order.add(u); for (int v : adj.get(u)) { if (--indeg[v] == 0) queue.add(v); } } return order.size() == n ? order : null; // null: a cycle exists } ``` ## C# ```csharp public static List TopoSort(int n, List> adj) { var indeg = new int[n]; foreach (var edges in adj) foreach (int v in edges) indeg[v]++; var queue = new Queue(); for (int i = 0; i < n; i++) if (indeg[i] == 0) queue.Enqueue(i); var order = new List(); while (queue.Count > 0) { int u = queue.Dequeue(); order.Add(u); foreach (int v in adj[u]) { if (--indeg[v] == 0) queue.Enqueue(v); } } return order.Count == n ? order : null; // null: a cycle exists } ``` --- title: "Система непересекающихся множеств" url: https://algopath.pro/ru/patterns/union-find language: ru summary: "Каждый элемент указывает на родителя, а подъём наверх приводит к корню, который называет группу. Элементы вместе, если корни совпали." updated: 2026-10-22 --- # Система непересекающихся множеств Каждый элемент указывает на родителя, а подъём наверх приводит к корню, который называет группу. Элементы вместе, если корни совпали. ## Система непересекающихся множеств: как это работает? Union-find, система непересекающихся множеств и DSU (disjoint set union) это одна и та же структура. Каждый элемент начинается своей группой. Его указатель ведёт на себя. find поднимается по цепочке родителей до корня. Корень и есть имя группы. union берёт два элемента, находит оба корня и вешает один под другой. Один вызов сливает две группы. Сжатие пути переписывает узлы на пути прямо на корень. Следующий find делает один шаг. Ранговое объединение вешает низкое дерево под высокое. Так цепочки не растут. С обоими приёмами find и union стоят почти константу. Рёбра при этом нигде не хранятся. Источники: [Википедия: Система непересекающихся множеств](https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D0%BD%D0%B5%D0%BF%D0%B5%D1%80%D0%B5%D1%81%D0%B5%D0%BA%D0%B0%D1%8E%D1%89%D0%B8%D1%85%D1%81%D1%8F_%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D1%81%D1%82%D0%B2), [Tarjan (1975), Efficiency of a good but not linear set union algorithm (англ.)](https://doi.org/10.1145/321879.321884) - `[0, 1, 2, 3, 4]` Пять элементов, пять групп. Каждый сам себе родитель. - `[0, 0, 2, 3, 4]` union(0, 1) вешает 1 под 0. У двух элементов общий корень. - `[0, 0, 2, 2, 4]` union(2, 3) вешает 3 под 2. Появилась вторая группа. - `[0, 0, 0, 2, 4]` union(1, 2) находит корни 0 и 2. Корень 2 уходит под корень 0. - `[0, 0, 0, 0, 4]` find(3) идёт 3, 2, 0. Сжатие ставит 3 сразу на 0. ## Система непересекающихся множеств: когда применять? Паттерн «Система непересекающихся множеств» нужен, когда в условии есть слова «лежат ли эти двое в одной группе», «факты о слиянии приходят по одному» или «связность меняется по мере добавления рёбер». Решение на нём обычно укладывается в near O(1) per op. - лежат ли эти двое в одной группе - факты о слиянии приходят по одному - связность меняется по мере добавления рёбер - склейка аккаунтов или друзей во времени - это ребро замкнёт цикл ## Система непересекающихся множеств: с чем путают? Паттерн «Система непересекающихся множеств» чаще всего путают с этими паттернами: Компоненты связности, Обход графа в ширину и глубину, Топологическая сортировка и Хеш-таблица и множество. - **Компоненты связности** - Заливка считает группы в уже готовом графе. Здесь ответ нужен, пока рёбра ещё приходят. - **Обход графа в ширину и глубину** - Обход ходит по соседям, значит нужен список смежности. Здесь рёбра не хранятся совсем. - **Топологическая сортировка** - Топологический порядок требует направленных рёбер без циклов. Здесь любое ребро ненаправленное. - **Хеш-таблица и множество** - Словарь группирует по заранее известному ключу. Здесь группы выясняются по мере слияний. ## Система непересекающихся множеств: сложность по времени и памяти У паттерна «Система непересекающихся множеств» целевая сложность near O(1) per op. n до 1e6 элементов и m перемешанных запросов дают O(m alpha(n)). Alpha меньше 5 при любом реальном n. ## Система непересекающихся множеств: разбор примера ### Лишнее ребро Дан граф, который был деревом, плюс одно лишнее ребро. Найдите ребро, замыкающее цикл. Рёбра приходят по порядку. Верните последнее, соединяющее уже связанные вершины. Идите по рёбрам подряд и объединяйте концы. Если у концов уже общий корень, ребро ничего не добавляет. Оно замыкает цикл. Запоминайте последнее такое ребро. Это и есть ответ. ```javascript function findRedundantConnection(edges) { const parent = Array.from({ length: edges.length + 1 }, (_, i) => i); function find(x) { while (parent[x] !== x) { parent[x] = parent[parent[x]]; // path halving x = parent[x]; } return x; } let answer = []; for (const [a, b] of edges) { const rootA = find(a); const rootB = find(b); if (rootA === rootB) { answer = [a, b]; // both ends were already connected } else { parent[rootA] = rootB; } } return answer; } ``` ## Система непересекающихся множеств: частые ошибки - **Сравнивают элементы, а не корни** Два элемента одной группы могут иметь разных родителей. Сравнивайте find(a) и find(b). - **union без find** Запись parent[a] = b соединяет два элемента, а не две группы. Вешайте всегда корень под корень. - **Пропускают сжатие пути** Без него цепочка вырастает до n звеньев. Один find тогда стоит O(n). - **Размер массива берут по числу рёбер** Массив индексируется элементами, а не рёбрами. Ошибка на единицу даёт undefined на последней вершине. ## Система непересекающихся множеств: задачи с собеседований Паттерн «Система непересекающихся множеств» решает такие задачи с собеседований: Число провинций, Лишнее ребро, Слияние аккаунтов, Число островов, Максимум убранных камней, Выполнимость равенств, Наименьшая строка обменами и Минимальная стоимость соединения всех точек. - **Число провинций** Объедините все связанные пары и посчитайте разные корни. - **Лишнее ребро** Первое ребро, у концов которого уже общий корень. - **Слияние аккаунтов** Почты связывают аккаунты. Объединяйте по общей почте, потом группируйте по корню. - **Число островов** Объединяйте соседние клетки суши. Удобно, когда сетка приходит частями. - **Максимум убранных камней** Камни в одной строке или колонке образуют группу. - **Выполнимость равенств** Сначала объедините равенства. Потом проверьте каждое неравенство по корням. - **Наименьшая строка обменами** Обмениваемые индексы образуют группу. Отсортируйте буквы внутри каждой группы. - **Минимальная стоимость соединения всех точек** Алгоритм Краскала: сортируйте рёбра по цене и берите ребро, если корни его концов разные. ## JavaScript ```javascript class UnionFind { constructor(n) { this.parent = Array.from({ length: n }, (_, i) => i); this.rank = new Array(n).fill(0); } find(x) { if (this.parent[x] !== x) this.parent[x] = this.find(this.parent[x]); return this.parent[x]; } union(x, y) { const rx = this.find(x), ry = this.find(y); if (rx === ry) return false; if (this.rank[rx] < this.rank[ry]) this.parent[rx] = ry; else if (this.rank[rx] > this.rank[ry]) this.parent[ry] = rx; else { this.parent[ry] = rx; this.rank[rx]++; } return true; } } ``` ## TypeScript ```typescript class UnionFind { private parent: number[]; private rank: number[]; constructor(n: number) { this.parent = Array.from({ length: n }, (_, i) => i); this.rank = new Array(n).fill(0); } find(x: number): number { if (this.parent[x] !== x) this.parent[x] = this.find(this.parent[x]); return this.parent[x]; } union(x: number, y: number): boolean { const rx = this.find(x); const ry = this.find(y); if (rx === ry) return false; if (this.rank[rx] < this.rank[ry]) this.parent[rx] = ry; else if (this.rank[rx] > this.rank[ry]) this.parent[ry] = rx; else { this.parent[ry] = rx; this.rank[rx]++; } return true; } } ``` ## Python ```python class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.rank = [0] * n def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): rx, ry = self.find(x), self.find(y) if rx == ry: return False if self.rank[rx] < self.rank[ry]: rx, ry = ry, rx self.parent[ry] = rx if self.rank[rx] == self.rank[ry]: self.rank[rx] += 1 return True ``` ## PHP ```php class UnionFind { private array $parent; private array $rank; public function __construct(int $n) { $this->parent = range(0, $n - 1); $this->rank = array_fill(0, $n, 0); } public function find(int $x): int { if ($this->parent[$x] !== $x) { $this->parent[$x] = $this->find($this->parent[$x]); } return $this->parent[$x]; } public function union(int $x, int $y): bool { [$rx, $ry] = [$this->find($x), $this->find($y)]; if ($rx === $ry) return false; if ($this->rank[$rx] < $this->rank[$ry]) [$rx, $ry] = [$ry, $rx]; $this->parent[$ry] = $rx; if ($this->rank[$rx] === $this->rank[$ry]) $this->rank[$rx]++; return true; } } ``` ## Java ```java public static class UnionFind { private final int[] parent; private final int[] rank; public UnionFind(int n) { parent = new int[n]; rank = new int[n]; for (int i = 0; i < n; i++) parent[i] = i; } public int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); return parent[x]; } public boolean union(int x, int y) { int rx = find(x), ry = find(y); if (rx == ry) return false; if (rank[rx] < rank[ry]) parent[rx] = ry; else if (rank[rx] > rank[ry]) parent[ry] = rx; else { parent[ry] = rx; rank[rx]++; } return true; } } ``` ## C# ```csharp public class UnionFind { private readonly int[] parent; private readonly int[] rank; public UnionFind(int n) { parent = new int[n]; rank = new int[n]; for (int i = 0; i < n; i++) parent[i] = i; } public int Find(int x) { if (parent[x] != x) parent[x] = Find(parent[x]); return parent[x]; } public bool Union(int x, int y) { int rx = Find(x), ry = Find(y); if (rx == ry) return false; if (rank[rx] < rank[ry]) parent[rx] = ry; else if (rank[rx] > rank[ry]) parent[ry] = rx; else { parent[ry] = rx; rank[rx]++; } return true; } } ``` --- title: "Алгоритм Дейкстры" url: https://algopath.pro/ru/patterns/dijkstra language: ru summary: "Закрывайте ближайшую незакрытую вершину и ослабляйте все её рёбра. Куча держит эту вершину всегда в одном чтении от вас." updated: 2026-10-17 --- # Алгоритм Дейкстры Закрывайте ближайшую незакрытую вершину и ослабляйте все её рёбра. Куча держит эту вершину всегда в одном чтении от вас. ## Алгоритм Дейкстры: как это работает? Дайте каждой вершине расстояние бесконечность. Стартовая получает ноль. Положите старт в кучу с ключом по расстоянию. Куча всегда отдаёт ближайшую вершину. Снимите вершину с кучи. Если сохранённое расстояние лучше снятого, пропустите её. Иначе вершина закрыта. Её расстояние окончательно и больше не улучшится. Для каждого исходящего ребра сравните сохранённое расстояние с маршрутом через эту вершину. Если новый короче, запишите его и положите в кучу. Повторяйте, пока куча не опустеет. Каждая достижимая вершина закрыта один раз. Источники: [Википедия: Алгоритм Дейкстры](https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%94%D0%B5%D0%B9%D0%BA%D1%81%D1%82%D1%80%D1%8B), [Dijkstra (1959), A note on two problems in connexion with graphs (англ.)](https://doi.org/10.1007/BF01386390) - `расстояния: A 0, B inf, C inf` Три ребра: A в B за 1, A в C за 4. Из B в C за 2. - `снимаем A и ослабляем` B становится 1, а C становится 4. - `снимаем B с 1, ослабляем C` Ноль плюс 1 плюс 2 это 3, и это лучше 4. - `расстояния: A 0, B 1, C 3` C кладётся в кучу заново с лучшим значением. - `снимаем C с 3, потом C с 4` Устаревшая запись пропускается, тройка уже лучше. ## Алгоритм Дейкстры: когда применять? Паттерн «Алгоритм Дейкстры» нужен, когда в условии есть слова «самый дешёвый/короткий путь со взвешенными рёбрами», «неотрицательные веса рёбер» или «минимальная суммарная стоимость до каждого узла». Решение на нём обычно укладывается в O(E log V). - самый дешёвый/короткий путь со взвешенными рёбрами - неотрицательные веса рёбер - минимальная суммарная стоимость до каждого узла - задержка в сети / стоимость маршрута, а не число дорог - взвешенный граф, кратчайшие пути из одного источника ## Алгоритм Дейкстры: с чем путают? Паттерн «Алгоритм Дейкстры» чаще всего путают с этими паттернами: Обход графа BFS / DFS, Бинарная куча / очередь с приоритетом, Жадный алгоритм (обменный аргумент) и Топологическая сортировка (алгоритм Кана). - **Обход графа BFS / DFS** - Очередь считает шаги и считает все рёбра одинаковыми. Куча умеет работать с настоящими весами. - **Бинарная куча / очередь с приоритетом** - Куча это инструмент, на котором всё держится. Та страница про сам контейнер. - **Жадный алгоритм (обменный аргумент)** - Закрывать ближайшую вершину это жадный выбор. Он верен, пока нет отрицательных весов. - **Топологическая сортировка (алгоритм Кана)** - В графе без циклов порядок ослабляет рёбра без всякой кучи. Так быстрее, и отрицательные веса допустимы. ## Алгоритм Дейкстры: сложность по времени и памяти У паттерна «Алгоритм Дейкстры» целевая сложность O(E log V). V вершин и E рёбер дают O(E log V) с кучей. Отрицательные веса её ломают, там нужен Беллман-Форд. ## Алгоритм Дейкстры: разбор примера ### За сколько сигнал дойдёт до всех Сигнал стартует в одной вершине и идёт по рёбрам с весами. Верните время, за которое он дойдёт до всех. Если какая-то вершина недостижима, верните минус один. Задача о сигнале это один запуск кратчайших путей из одного источника. Ответ это наибольшее из итоговых расстояний. Недостижимая вершина оставит бесконечность. ```javascript function networkDelayTime(times, n, k) { const graph = new Map(); for (const [from, to, weight] of times) { if (!graph.has(from)) graph.set(from, []); graph.get(from).push([to, weight]); } const dist = new Map([[k, 0]]); const heap = new MinHeap((entry) => entry[0]); // entries are [distance, node] heap.push([0, k]); while (heap.size()) { const [d, node] = heap.pop(); if (d > (dist.get(node) ?? Infinity)) continue; // a stale entry for (const [next, weight] of graph.get(node) ?? []) { const candidate = d + weight; if (candidate < (dist.get(next) ?? Infinity)) { dist.set(next, candidate); heap.push([candidate, next]); } } } if (dist.size < n) return -1; return Math.max(...dist.values()); } ``` ## Алгоритм Дейкстры: частые ошибки - **Запускают на отрицательных весах** Закрытая вершина всё ещё может улучшиться, и жадный шаг рушится. Там нужен Беллман-Форд. - **Не пропускают устаревшие записи кучи** Вершину кладут несколько раз с разными расстояниями. При снятии сравнивайте с сохранённым значением. - **Закрывают вершину при вставке** Рано положенная вершина позже может получить путь лучше. Закрывайте при снятии, а не при вставке. - **Берут обычную очередь** Очередь считает рёбра, а не вес. Это совпадает, только если все рёбра стоят одинаково. ## Алгоритм Дейкстры: задачи с собеседований Паттерн «Алгоритм Дейкстры» решает такие задачи с собеседований: Время задержки в сети, Дешёвый перелёт с k пересадками, Путь с наименьшим усилием, Плавание в поднимающейся воде, Путь с наибольшей вероятностью, Минимальная стоимость до последней клетки и Второй кратчайший путь. - **Время задержки в сети** Чистая форма: ответ это наибольшее расстояние. - **Дешёвый перелёт с k пересадками** Лимит пересадок добавляет к состоянию второе измерение. - **Путь с наименьшим усилием** Стоимость пути это его худший шаг. - **Плавание в поднимающейся воде** То же самое, но с максимумом вместо суммы. - **Путь с наибольшей вероятностью** Здесь умножают, а не складывают, и берут наибольшее. - **Минимальная стоимость до последней клетки** Сетка это граф с четырьмя рёбрами на клетку. - **Второй кратчайший путь** Храните два лучших расстояния на вершину. ## JavaScript ```javascript function dijkstra(n, adj, source) { const dist = new Array(n).fill(Infinity); dist[source] = 0; const heap = [[0, source]]; // [distance, node] while (heap.length) { heap.sort((a, b) => a[0] - b[0]); // swap for a real heap in production const [d, u] = heap.shift(); if (d > dist[u]) continue; for (const [v, w] of adj[u] || []) { if (d + w < dist[v]) { dist[v] = d + w; heap.push([dist[v], v]); } } } return dist; } ``` ## TypeScript ```typescript function dijkstra(n: number, adj: Array>, source: number): number[] { const dist = new Array(n).fill(Infinity); dist[source] = 0; const heap: Array<[number, number]> = [[0, source]]; // [distance, node] while (heap.length) { heap.sort((a, b) => a[0] - b[0]); // swap for a real heap in production const [d, u] = heap.shift()!; if (d > dist[u]) continue; for (const [v, w] of adj[u] ?? []) { if (d + w < dist[v]) { dist[v] = d + w; heap.push([dist[v], v]); } } } return dist; } ``` ## Python ```python import heapq def dijkstra(n, adj, source): dist = [float('inf')] * n dist[source] = 0 heap = [(0, source)] while heap: d, u = heapq.heappop(heap) if d > dist[u]: continue for v, w in adj.get(u, []): if d + w < dist[v]: dist[v] = d + w heapq.heappush(heap, (dist[v], v)) return dist ``` ## PHP ```php function dijkstra(int $n, array $adj, int $source): array { $dist = array_fill(0, $n, INF); $dist[$source] = 0; $heap = new SplPriorityQueue(); $heap->insert($source, 0); while (!$heap->isEmpty()) { $u = $heap->extract(); foreach ($adj[$u] ?? [] as [$v, $w]) { if ($dist[$u] + $w < $dist[$v]) { $dist[$v] = $dist[$u] + $w; $heap->insert($v, -$dist[$v]); // max-heap: negate for min-order } } } return $dist; } ``` ## Java ```java public static int[] dijkstra(int n, List> adj, int source) { int[] dist = new int[n]; Arrays.fill(dist, Integer.MAX_VALUE); dist[source] = 0; // {distance, node}, smallest distance first PriorityQueue heap = new PriorityQueue<>(Comparator.comparingInt(a -> a[0])); heap.add(new int[] { 0, source }); while (!heap.isEmpty()) { int[] top = heap.poll(); int d = top[0], u = top[1]; if (d > dist[u]) continue; for (int[] edge : adj.get(u)) { int v = edge[0], w = edge[1]; if (d + w < dist[v]) { dist[v] = d + w; heap.add(new int[] { dist[v], v }); } } } return dist; } ``` ## C# ```csharp public static int[] Dijkstra(int n, List> adj, int source) { var dist = new int[n]; Array.Fill(dist, int.MaxValue); dist[source] = 0; // the node, keyed by its distance: smallest first var heap = new PriorityQueue(); heap.Enqueue(source, 0); while (heap.Count > 0) { heap.TryDequeue(out int u, out int d); if (d > dist[u]) continue; foreach (var edge in adj[u]) { int v = edge[0], w = edge[1]; if (d + w < dist[v]) { dist[v] = d + w; heap.Enqueue(v, dist[v]); } } } return dist; } ``` --- title: "Эйлеров путь (алгоритм Хирхольцера)" url: https://algopath.pro/ru/patterns/eulerian-path language: ru summary: "Пройдите граф, использовав каждое ребро ровно один раз. Кладите вершину в стек, когда она застряла, а ответ читайте с конца." updated: 2026-10-17 --- # Эйлеров путь (алгоритм Хирхольцера) Пройдите граф, использовав каждое ребро ровно один раз. Кладите вершину в стек, когда она застряла, а ответ читайте с конца. ## Эйлеров путь (алгоритм Хирхольцера): как это работает? Сначала проверьте степени вершин. Для ненаправленного пути нечётную степень могут иметь не больше двух вершин. В ориентированном графе лишнее исходящее ребро может быть только у одной вершины. Она и обязана быть началом. Идите вперёд жадно и удаляйте каждое использованное ребро. По использованному ребру больше не ходят. Когда у вершины не осталось рёбер, положите её в выходной стек. Она застряла, значит её место ближе к концу. Вернитесь в предыдущую вершину и продолжайте. Неиспользованные рёбра там всё ещё доступны. В самом конце разверните выходной стек. Это ставит путь в правильном порядке. Источники: [Википедия: Эйлеров цикл](https://ru.wikipedia.org/wiki/%D0%AD%D0%B9%D0%BB%D0%B5%D1%80%D0%BE%D0%B2_%D1%86%D0%B8%D0%BA%D0%BB) - `в JFK, рёбра в A и в B` Берём A первым, если нужен наименьший маршрут. - `в A, потом обратно в JFK` Единственное ребро из A возвращает в JFK. - `в B: рёбер не осталось` B застряла, поэтому кладётся первой. - `кладём JFK, A, JFK` Каждая вершина кладётся, когда исчерпала рёбра. - `развернули: JFK, A, JFK, B` Чтение стека с конца даёт маршрут. ## Эйлеров путь (алгоритм Хирхольцера): когда применять? Паттерн «Эйлеров путь (алгоритм Хирхольцера)» нужен, когда в условии есть слова «использовать каждое ребро ровно один раз», «восстановить маршрут по билетам» или «рисунок одним росчерком / не отрывая ручку». Решение на нём обычно укладывается в O(E). - использовать каждое ребро ровно один раз - восстановить маршрут по билетам - рисунок одним росчерком / не отрывая ручку - пройти каждую дорогу/домино ровно один раз - рейсы или домино должны соединиться в одну поездку ## Эйлеров путь (алгоритм Хирхольцера): с чем путают? Паттерн «Эйлеров путь (алгоритм Хирхольцера)» чаще всего путают с этими паттернами: Обход графа BFS / DFS, Бэктрекинг, Компоненты связности и Стек (LIFO). - **Обход графа BFS / DFS** - Обходу нужно достичь каждой вершины. Здесь нужно использовать каждое ребро. - **Бэктрекинг** - Пробовать маршруты и откатывать их это экспонента. Этот обход шагов назад не делает. - **Компоненты связности** - Все рёбра обязаны лежать в одной компоненте, и это одно из условий. Но считать группы здесь не цель. - **Стек (LIFO)** - Стек это то, на чём обход держится. Эта страница про то, когда в него класть. ## Эйлеров путь (алгоритм Хирхольцера): сложность по времени и памяти У паттерна «Эйлеров путь (алгоритм Хирхольцера)» целевая сложность O(E). V вершин и E рёбер дают O(E log E) вместе с сортировкой рёбер. Без сортировки это O(E). ## Эйлеров путь (алгоритм Хирхольцера): разбор примера ### Восстановить маршрут перелётов Дан список авиабилетов, каждый надо использовать ровно один раз, начиная с JFK. Если маршрутов несколько, верните наименьший по алфавиту. Отсортируйте пункты назначения каждого аэропорта, чтобы первым брался наименьший. Идите жадно и кладите аэропорт в стек, когда билетов не осталось. В конце разверните стек. ```javascript function findItinerary(tickets) { const graph = new Map(); for (const [from, to] of tickets) { if (!graph.has(from)) graph.set(from, []); graph.get(from).push(to); } // reversed, so pop() hands back the smallest destination for (const list of graph.values()) list.sort().reverse(); const route = []; const stack = ["JFK"]; while (stack.length) { const airport = stack[stack.length - 1]; const next = graph.get(airport); if (next && next.length) { stack.push(next.pop()); // the ticket is used up } else { route.push(stack.pop()); // stuck, so this airport ends the route } } return route.reverse(); } ``` ## Эйлеров путь (алгоритм Хирхольцера): частые ошибки - **Возвращают путь в порядке обхода** Выходной стек обязан быть развёрнут. Порядок обхода ставит тупики не туда. - **Не удаляют использованное ребро** Тогда по одному ребру ходят бесконечно. Убирайте его в момент использования. - **Пропускают проверку степеней** У множества графов такого обхода вообще нет. Проверьте степени до начала. - **Берут бэктрекинг** Перебор маршрутов с откатом взрывается на большом графе. Правилу со стеком откаты не нужны. ## Эйлеров путь (алгоритм Хирхольцера): задачи с собеседований Паттерн «Эйлеров путь (алгоритм Хирхольцера)» решает такие задачи с собеседований: Восстановление маршрута, Верная расстановка пар, Взлом сейфа, Проверка эйлерова цикла, Цепочка домино, Сборка генома из фрагментов и Кёнигсбергские мосты. - **Восстановление маршрута** Направленные рёбра, при равенстве берётся алфавитный порядок. - **Верная расстановка пар** Начало определяется по степеням вершин. - **Взлом сейфа** Последовательность де Брёйна это эйлеров цикл. - **Проверка эйлерова цикла** Все степени чётные и одна компонента связности. - **Цепочка домино** Каждая костяшка это ребро между двумя числами. - **Сборка генома из фрагментов** В графе перекрытий лежит эйлеров путь. - **Кёнигсбергские мосты** Исходное доказательство того, что такого обхода нет. ## JavaScript ```javascript function eulerianPath(tickets, start) { const adj = {}; for (const [from, to] of tickets) { (adj[from] ||= []).push(to); } for (const from in adj) adj[from].sort().reverse(); // pop smallest first const route = []; const stack = [start]; while (stack.length) { const node = stack[stack.length - 1]; if (adj[node] && adj[node].length) { stack.push(adj[node].pop()); } else { route.push(stack.pop()); // dead end: record and back up } } return route.reverse(); } ``` ## TypeScript ```typescript function eulerianPath(tickets: string[][], start: string): string[] { const adj: Record = {}; for (const [from, to] of tickets) { (adj[from] ??= []).push(to); } for (const from in adj) adj[from].sort().reverse(); // pop smallest first const route: string[] = []; const stack: string[] = [start]; while (stack.length) { const dests = adj[stack[stack.length - 1]] ?? []; if (dests.length) { stack.push(dests.pop()!); } else { route.push(stack.pop()!); // dead end: record and back up } } return route.reverse(); } ``` ## Python ```python def eulerian_path(tickets, start): adj = {} for frm, to in tickets: adj.setdefault(frm, []).append(to) for frm in adj: adj[frm].sort(reverse=True) # pop smallest first route = [] stack = [start] while stack: node = stack[-1] if adj.get(node): stack.append(adj[node].pop()) else: route.append(stack.pop()) # dead end: record and back up return route[::-1] ``` ## PHP ```php function eulerianPath(array $tickets, string $start): array { $adj = []; foreach ($tickets as [$from, $to]) { $adj[$from][] = $to; } foreach ($adj as $from => &$list) { rsort($list); // pop smallest first } unset($list); $route = []; $stack = [$start]; while ($stack) { $node = end($stack); if (!empty($adj[$node])) { $stack[] = array_pop($adj[$node]); } else { $route[] = array_pop($stack); // dead end: record and back up } } return array_reverse($route); } ``` ## Java ```java public static List eulerianPath(String[][] tickets, String start) { Map> adj = new HashMap<>(); for (String[] ticket : tickets) { adj.computeIfAbsent(ticket[0], k -> new ArrayList<>()).add(ticket[1]); } for (List dests : adj.values()) { dests.sort(Comparator.reverseOrder()); // pop smallest first } List route = new ArrayList<>(); Deque stack = new ArrayDeque<>(); stack.push(start); while (!stack.isEmpty()) { List dests = adj.get(stack.peek()); if (dests != null && !dests.isEmpty()) { stack.push(dests.remove(dests.size() - 1)); } else { route.add(stack.pop()); // dead end: record and back up } } Collections.reverse(route); return route; } ``` ## C# ```csharp public static List EulerianPath(string[][] tickets, string start) { var adj = new Dictionary>(); foreach (var ticket in tickets) { if (!adj.ContainsKey(ticket[0])) adj[ticket[0]] = new List(); adj[ticket[0]].Add(ticket[1]); } foreach (var dests in adj.Values) { dests.Sort(); dests.Reverse(); // pop smallest first } var route = new List(); var stack = new Stack(); stack.Push(start); while (stack.Count > 0) { if (adj.TryGetValue(stack.Peek(), out var dests) && dests.Count > 0) { string next = dests[dests.Count - 1]; dests.RemoveAt(dests.Count - 1); stack.Push(next); } else { route.Add(stack.Pop()); // dead end: record and back up } } route.Reverse(); return route; } ``` --- title: "Проверка двудольности (раскраска в два цвета)" url: https://algopath.pro/ru/patterns/bipartite language: ru summary: "Покрасьте любую вершину, а потом покрасьте каждого её соседа в другой цвет. Конфликт доказывает, что граф не двудольный." updated: 2026-10-17 --- # Проверка двудольности (раскраска в два цвета) Покрасьте любую вершину, а потом покрасьте каждого её соседа в другой цвет. Конфликт доказывает, что граф не двудольный. ## Проверка двудольности (раскраска в два цвета): как это работает? В начале все вершины без цвета. Дальше пройдите по ним всем. Непокрашенная вершина открывает новый обход. Покрасьте её первым цветом. Обойдите её соседей по очереди. Непокрашенного соседа красьте в противоположный цвет. Уже покрашенный сосед обязан отличаться от текущей вершины. Если цвета совпали, граф не проходит. Конфликт означает, что где-то есть нечётный цикл. Никакая раскраска в два цвета его не переживёт. Повторите для каждой компоненты. Граф двудольный, только если прошли все. Источники: [Википедия: Двудольный граф](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D1%83%D0%B4%D0%BE%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D0%B3%D1%80%D0%B0%D1%84) - `цвет[0] = A` Граф это треугольник: 0-1, 1-2 и 2-0. - `цвет[1] = B` Сосед получает другой цвет. - `цвет[2] = A` Вершина 2 соседствует с 1, поэтому берёт A. - `ребро 2-0: A против A` На обоих концах один цвет. Проверка провалилась. - `ответ = ложь` Треугольник это нечётный цикл. Два цвета в него не помещаются. ## Проверка двудольности (раскраска в два цвета): когда применять? Паттерн «Проверка двудольности (раскраска в два цвета)» нужен, когда в условии есть слова «разбить на две группы без конфликта внутри группы», «раскрасить граф в два цвета» или «обнаружить цикл нечётной длины». Решение на нём обычно укладывается в O(V+E). - разбить на две группы без конфликта внутри группы - раскрасить граф в два цвета - обнаружить цикл нечётной длины - мы против них / расписание на две смены / распределение по двум комнатам - возможно ли двудольное разбиение конфликтующих пар ## Проверка двудольности (раскраска в два цвета): с чем путают? Паттерн «Проверка двудольности (раскраска в два цвета)» чаще всего путают с этими паттернами: Обход графа BFS / DFS, Компоненты связности, Система непересекающихся множеств и Бэктрекинг. - **Обход графа BFS / DFS** - Сам обход точно такой же. Добавился только цвет, который несёт каждая вершина. - **Компоненты связности** - Оба перебирают непосещённые вершины и запускают обход. Здесь обход ещё и может провалиться. - **Система непересекающихся множеств** - Взвешенное непересекающееся множество отвечает на тот же вопрос. Оно удобно, когда рёбра приходят со временем. - **Бэктрекинг** - Для трёх и более цветов нужен перебор с откатом. Двум цветам выбор не нужен вовсе. ## Проверка двудольности (раскраска в два цвета): сложность по времени и памяти У паттерна «Проверка двудольности (раскраска в два цвета)» целевая сложность O(V+E). V вершин и E рёбер дают O(V + E). Каждая вершина красится один раз, каждое ребро проверяется дважды. ## Проверка двудольности (раскраска в два цвета): разбор примера ### Разделить людей на две группы Дано число людей и список пар, которые не выносят друг друга. Разделите всех на две группы. В одной группе не должно оказаться двух неприятных друг другу людей. Каждый человек это вершина, а каждая неприязнь это ребро. Запустите раскраску в два цвета по всем компонентам. Один конфликт делает разделение невозможным. ```javascript function possibleBipartition(n, dislikes) { const graph = Array.from({ length: n + 1 }, () => []); for (const [a, b] of dislikes) { graph[a].push(b); graph[b].push(a); } const colour = new Array(n + 1).fill(0); for (let start = 1; start <= n; start++) { if (colour[start] !== 0) continue; // already placed by an earlier walk colour[start] = 1; const queue = [start]; for (let i = 0; i < queue.length; i++) { const node = queue[i]; for (const next of graph[node]) { if (colour[next] === colour[node]) return false; // same side if (colour[next] === 0) { colour[next] = -colour[node]; queue.push(next); } } } } return true; } ``` ## Проверка двудольности (раскраска в два цвета): частые ошибки - **Идут только от первой вершины** В несвязном графе конфликт может прятаться в другой части. Запускайте обход из каждой непокрашенной вершины. - **Хранят только флаг посещения** Нужен цвет, а не просто факт посещения. Один массив может нести и то, и другое. - **Красят при извлечении из очереди** Тогда вершина попадает в очередь дважды с разными цветами. Красьте её при вставке. - **Ждут, что провал выглядит как-то особенно** Единственная его причина это нечётный цикл. Граф только с чётными циклами проходит всегда. ## Проверка двудольности (раскраска в два цвета): задачи с собеседований Паттерн «Проверка двудольности (раскраска в два цвета)» решает такие задачи с собеседований: Двудольный ли граф, Возможное разбиение на две группы, Разделение игроков на две команды, Поиск нечётного цикла, Максимальное паросочетание в двудольном графе, Посадка цветов без одинаковых соседей и Раскраска графа в три цвета. - **Двудольный ли граф** Чистая форма, по списку смежности. - **Возможное разбиение на две группы** Рёбрами становятся пары неприязни. - **Разделение игроков на две команды** Та же раскраска в два цвета, другими словами. - **Поиск нечётного цикла** Та же проверка, заданная с другой стороны. - **Максимальное паросочетание в двудольном графе** Имеет смысл, только когда доли уже известны. - **Посадка цветов без одинаковых соседей** Четыре цвета, поэтому хватает жадного прохода. - **Раскраска графа в три цвета** Бэктрекинг, поскольку двух цветов уже не хватает. ## JavaScript ```javascript function isBipartite(n, adj) { const color = new Array(n).fill(-1); for (let start = 0; start < n; start++) { if (color[start] !== -1) continue; color[start] = 0; const queue = [start]; while (queue.length) { const node = queue.shift(); for (const next of adj[node] || []) { if (color[next] === -1) { color[next] = 1 - color[node]; queue.push(next); } else if (color[next] === color[node]) { return false; // same color on both ends: odd cycle } } } } return true; } ``` ## TypeScript ```typescript function isBipartite(n: number, adj: number[][]): boolean { const color = new Array(n).fill(-1); for (let start = 0; start < n; start++) { if (color[start] !== -1) continue; color[start] = 0; const queue: number[] = [start]; while (queue.length) { const node = queue.shift()!; for (const next of adj[node] ?? []) { if (color[next] === -1) { color[next] = 1 - color[node]; queue.push(next); } else if (color[next] === color[node]) { return false; // same color on both ends: odd cycle } } } } return true; } ``` ## Python ```python from collections import deque def is_bipartite(n, adj): color = [-1] * n for start in range(n): if color[start] != -1: continue color[start] = 0 queue = deque([start]) while queue: node = queue.popleft() for nxt in adj.get(node, []): if color[nxt] == -1: color[nxt] = 1 - color[node] queue.append(nxt) elif color[nxt] == color[node]: return False # same color on both ends: odd cycle return True ``` ## PHP ```php function isBipartite(int $n, array $adj): bool { $color = array_fill(0, $n, -1); for ($start = 0; $start < $n; $start++) { if ($color[$start] !== -1) continue; $color[$start] = 0; $queue = [$start]; while ($queue) { $node = array_shift($queue); foreach ($adj[$node] ?? [] as $next) { if ($color[$next] === -1) { $color[$next] = 1 - $color[$node]; $queue[] = $next; } elseif ($color[$next] === $color[$node]) { return false; // same color on both ends: odd cycle } } } } return true; } ``` ## Java ```java public static boolean isBipartite(int n, List> adj) { int[] color = new int[n]; Arrays.fill(color, -1); for (int start = 0; start < n; start++) { if (color[start] != -1) continue; color[start] = 0; Queue queue = new ArrayDeque<>(); queue.add(start); while (!queue.isEmpty()) { int node = queue.poll(); for (int next : adj.get(node)) { if (color[next] == -1) { color[next] = 1 - color[node]; queue.add(next); } else if (color[next] == color[node]) { return false; // same color on both ends: odd cycle } } } } return true; } ``` ## C# ```csharp public static bool IsBipartite(int n, List> adj) { var color = new int[n]; Array.Fill(color, -1); for (int start = 0; start < n; start++) { if (color[start] != -1) continue; color[start] = 0; var queue = new Queue(); queue.Enqueue(start); while (queue.Count > 0) { int node = queue.Dequeue(); foreach (int next in adj[node]) { if (color[next] == -1) { color[next] = 1 - color[node]; queue.Enqueue(next); } else if (color[next] == color[node]) { return false; // same color on both ends: odd cycle } } } } return true; } ``` --- title: "Жадный алгоритм (обменный аргумент)" url: https://algopath.pro/ru/patterns/greedy language: ru summary: "Берите лучший на вид вариант прямо сейчас и никогда к нему не возвращайтесь. Работает это, только если есть доказательство." updated: 2026-10-17 --- # Жадный алгоритм (обменный аргумент) Берите лучший на вид вариант прямо сейчас и никогда к нему не возвращайтесь. Работает это, только если есть доказательство. ## Жадный алгоритм (обменный аргумент): как это работает? Назовите выбор, который делается на каждом шаге. Обычно это наименьший, наибольший или самый ранний вариант. Отсортируйте вход так, чтобы этот вариант был впереди. Сортировка и есть основная работа. Пройдите список один раз и берите всё, что подходит. Не оглядывайтесь на уже сделанный выбор. Весь приём держится на одном утверждении. Локально лучший выбор не закрывает глобально лучший ответ. Доказывают это обменом: возьмите любой оптимальный ответ и подставьте свой выбор. Если он остался оптимальным, жадность безопасна. Без такого доказательства жадность падает молча. Она вернёт правдоподобный ответ, который не лучший. Источники: [Википедия: Жадный алгоритм](https://ru.wikipedia.org/wiki/%D0%96%D0%B0%D0%B4%D0%BD%D1%8B%D0%B9_%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC) - `по концу: [1,3], [2,4], [3,5]` Бронируем встречи. Впереди та, что кончается раньше. - `берём [1, 3]` Она кончается раньше всех и оставляет больше места. - `пропускаем [2, 4]` Она начинается в 2, до конца предыдущей. - `берём [3, 5]` Она начинается ровно тогда, когда комната освободилась. - `ответ = 2` Помещаются две встречи. Сортировка по длине могла бы потерять одну. ## Жадный алгоритм (обменный аргумент): когда применять? Паттерн «Жадный алгоритм (обменный аргумент)» нужен, когда в условии есть слова «наименьшее число монет или частей на сумму», «берём самую крупную, что ещё помещается» или «выдать сдачу номиналами». Решение на нём обычно укладывается в O(n log n). - наименьшее число монет или частей на сумму - берём самую крупную, что ещё помещается - выдать сдачу номиналами - доказать оптимальность обменным аргументом ## Жадный алгоритм (обменный аргумент): с чем путают? Паттерн «Жадный алгоритм (обменный аргумент)» чаще всего путают с этими паттернами: Динамическое программирование (1-D), Бэктрекинг, Сортировка с пользовательским компаратором и Бинарный поиск по ответу. - **Динамическое программирование (1-D)** - Динамика держит все варианты открытыми до конца. Жадность выбирает сразу и не может откатиться. - **Бэктрекинг** - Бэктрекинг пробует все варианты и оставляет лучший. Жадность пробует один и ему верит. - **Сортировка с пользовательским компаратором** - Большинство жадных решений начинается с сортировки. В компараторе и записан сам выбор. - **Бинарный поиск по ответу** - Там сама проверка часто жадная. Поиск лишь выбирает значение для проверки. ## Жадный алгоритм (обменный аргумент): сложность по времени и памяти У паттерна «Жадный алгоритм (обменный аргумент)» целевая сложность O(n log n). O(n log n), если вход сначала нужно отсортировать. Проход после сортировки это один цикл, O(n). ## Жадный алгоритм (обменный аргумент): разбор примера ### Наименьшее число стрел для шаров Шары занимают горизонтальные отрезки. Стрела, выпущенная в точке x, лопает все шары, накрывающие x. Верните наименьшее число стрел, лопающих все шары. Отсортируйте шары по правому краю. Стреляйте в первый правый край и пропускайте все накрытые шары. Потом стреляйте в следующий не накрытый край. ```javascript function findMinArrowShots(points) { points.sort((a, b) => a[1] - b[1]); // by right edge, never by left let arrows = 1; let shot = points[0][1]; // the earliest right edge for (const [start, end] of points) { if (start > shot) { arrows++; shot = end; // this balloon is out of reach of the last arrow } } return arrows; } ``` ## Жадный алгоритм (обменный аргумент): частые ошибки - **Пропускают доказательство** Жадное правило, звучащее верно, очень часто неверно. Сначала поищите контрпример. - **Сортируют не по тому ключу** Ранний старт и ранний конец дают разные ответы. Ключ выбирает доказательство, а не интуиция. - **Берут жадность там, где выборы связаны** Размен монет со странными номиналами её ломает. Там безопасен только динамический подход. - **Пытаются отменить выбор** Как только жадность что-то забирает назад, это уже перебор. Пишите бэктрекинг или динамику. ## Жадный алгоритм (обменный аргумент): задачи с собеседований Паттерн «Жадный алгоритм (обменный аргумент)» решает такие задачи с собеседований: Непересекающиеся интервалы, Наименьшее число стрел, Прыжки по массиву, Заправки по кругу, Планировщик задач, Разбиение строки на части и Раздача печенья. - **Непересекающиеся интервалы** Оставляйте тот интервал, что кончается раньше. - **Наименьшее число стрел** По одной стреле на группу пересечений. - **Прыжки по массиву** Следите за самым дальним достижимым индексом. - **Заправки по кругу** Начинайте заново там, где бак ушёл в минус. - **Планировщик задач** В каждом раунде ставьте самую частую задачу. - **Разбиение строки на части** Режьте, как только все встреченные буквы закончились. - **Раздача печенья** Давайте ребёнку наименьшее подходящее печенье. ## JavaScript ```javascript function minCoinsGreedy(coins, amount) { const sorted = [...coins].sort((a, b) => b - a); const used = []; for (const coin of sorted) { while (amount >= coin) { amount -= coin; used.push(coin); } } return amount === 0 ? used : null; } ``` ## TypeScript ```typescript function minCoinsGreedy(coins: number[], amount: number): number[] | null { const sorted = [...coins].sort((a, b) => b - a); const used: number[] = []; for (const coin of sorted) { while (amount >= coin) { amount -= coin; used.push(coin); } } return amount === 0 ? used : null; } ``` ## Python ```python def min_coins_greedy(coins, amount): used = [] for coin in sorted(coins, reverse=True): while amount >= coin: amount -= coin used.append(coin) return used if amount == 0 else None ``` ## PHP ```php function minCoinsGreedy(array $coins, int $amount): ?array { rsort($coins); $used = []; foreach ($coins as $coin) { while ($amount >= $coin) { $amount -= $coin; $used[] = $coin; } } return $amount === 0 ? $used : null; } ``` ## Java ```java public static List minCoinsGreedy(int[] coins, int amount) { int[] sorted = coins.clone(); Arrays.sort(sorted); // ascending, so walk it backwards for the biggest first List used = new ArrayList<>(); for (int i = sorted.length - 1; i >= 0; i--) { int coin = sorted[i]; while (amount >= coin) { amount -= coin; used.add(coin); } } return amount == 0 ? used : null; } ``` ## C# ```csharp public static List MinCoinsGreedy(int[] coins, int amount) { var sorted = (int[]) coins.Clone(); Array.Sort(sorted); Array.Reverse(sorted); // biggest coin first var used = new List(); foreach (int coin in sorted) { while (amount >= coin) { amount -= coin; used.Add(coin); } } return amount == 0 ? used : null; } ``` --- title: "Динамическое программирование (1-D)" url: https://algopath.pro/ru/patterns/dynamic-programming language: ru summary: "Ответьте на маленькую версию задачи, сохраните ответ и стройте из него следующий. Каждое состояние считается только один раз." updated: 2026-10-17 --- # Динамическое программирование (1-D) Ответьте на маленькую версию задачи, сохраните ответ и стройте из него следующий. Каждое состояние считается только один раз. ## Динамическое программирование (1-D): как это работает? Решите, что означает одно состояние. Обычно это лучший ответ по первым i элементам. Запишите переход: как состояние i следует из более ранних. Этот переход и есть весь алгоритм. Базовые случаи заполните вручную. Это состояния, под которыми нет ничего меньшего. Идите вперёд и заполняйте таблицу по порядку. Всё, что она читает, уже посчитано. Ответ лежит в одной ячейке, обычно в последней. Иногда это наибольшая ячейка таблицы. Если переход смотрит только на два состояния назад, хватит двух переменных. Массив тогда не нужен. Источники: [Википедия: Динамическое программирование](https://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5), [Bellman (1954), The theory of dynamic programming (англ.)](https://doi.org/10.1090/S0002-9904-1954-09848-8) - `dp[0] = 2` Грабим дома [2, 7, 9]. Только первый даёт 2. - `dp[1] = 7` Берём лучшее из 2 и 7. Соседние дома вместе брать нельзя. - `dp[2] = max(7, 2 + 9)` Либо пропустить третий дом, либо взять его вместе с dp[0]. - `dp[2] = 11` Выигрывает вариант с первым и третьим домом. - `ответ = 11` Три состояния, каждое посчитано ровно один раз. ## Динамическое программирование (1-D): когда применять? Паттерн «Динамическое программирование (1-D)» нужен, когда в условии есть слова «лучшее/максимум/минимум, заканчивающееся в позиции i», «число способов дойти, подняться или замостить» или «нельзя брать два соседних элемента». Решение на нём обычно укладывается в O(states). - лучшее/максимум/минимум, заканчивающееся в позиции i - число способов дойти, подняться или замостить - нельзя брать два соседних элемента - минимальная стоимость пути по сетке - повторяющиеся подзадачи, одно и то же состояние считается заново ## Динамическое программирование (1-D): с чем путают? Паттерн «Динамическое программирование (1-D)» чаще всего путают с этими паттернами: Рекурсия с мемоизацией, Жадный алгоритм (обменный аргумент), Рекурсия и DP над подпоследовательностями (LIS / LCS / расстояние редактирования). - **Рекурсия с мемоизацией** - Мемоизация заполняет ту же таблицу сверху и по требованию. Здесь она заполняется снизу в заданном порядке. - **Жадный алгоритм (обменный аргумент)** - Жадность выбирает один вариант на шаг. Динамика держит все варианты до конца. - **Рекурсия** - Рекурсия показывает, каким будет переход. Скорость даёт именно таблица. - **DP над подпоследовательностями (LIS / LCS / расстояние редактирования)** - Там состояние это пара позиций в двух последовательностях. Здесь хватает одного индекса. ## Динамическое программирование (1-D): сложность по времени и памяти У паттерна «Динамическое программирование (1-D)» целевая сложность O(states). n состояний по O(1) работы дают O(n). Память падает до O(1), когда важны только последние состояния. ## Динамическое программирование (1-D): разбор примера ### Наибольшая сумма непрерывного отрезка Верните наибольшую сумму любого непрерывного отрезка массива. Значения бывают отрицательными, поэтому ответ это не просто общая сумма. Пусть состояние i это лучшая сумма отрезка, кончающегося на индексе i. Либо прошлый отрезок продолжается, либо здесь начинается новый. Ответ это наибольшее состояние. ```javascript function maxSubArray(nums) { let best = nums[0]; let endingHere = nums[0]; // only the previous state matters, so no array for (let i = 1; i < nums.length; i++) { // continue the stretch, or start a new one right here endingHere = Math.max(nums[i], endingHere + nums[i]); best = Math.max(best, endingHere); } return best; } ``` ## Динамическое программирование (1-D): частые ошибки - **Состояние не определяет будущее** Если две ситуации с одним состоянием ведут себя по-разному, состояние неполное. Добавьте недостающее. - **Заполняют таблицу в неверном порядке** Ячейку надо записать до того, как её прочитают. Идите в направлении, которого требует переход. - **Читают ответом не ту ячейку** Для состояния лучший-кончающийся-здесь ответом служит максимум. Это не последняя ячейка. - **Забывают базовые случаи** Пустой вход или вход из одного элемента ломают большинство решений. Разберите их первыми. ## Динамическое программирование (1-D): задачи с собеседований Паттерн «Динамическое программирование (1-D)» решает такие задачи с собеседований: Подъём по лестнице, Ограбление домов, Максимальный подмассив, Размен монет, Число способов расшифровки, Минимальная стоимость подъёма и Числа Фибоначчи. - **Подъём по лестнице** Каждая ступень приходит с одной или двух назад. - **Ограбление домов** Взять этот дом вместе с домом через один или пропустить. - **Максимальный подмассив** Продолжить отрезок или начать новый. - **Размен монет** Состояние это сумма, а каждая монета это ветка. - **Число способов расшифровки** Одна цифра или две, если пара допустима. - **Минимальная стоимость подъёма** Та же форма, но со стоимостью каждой ступени. - **Числа Фибоначчи** Самый короткий переход из всех возможных. ## JavaScript ```javascript function maxSubarraySum(nums) { let best = nums[0]; let curr = nums[0]; for (let i = 1; i < nums.length; i++) { curr = Math.max(nums[i], curr + nums[i]); best = Math.max(best, curr); } return best; } ``` ## TypeScript ```typescript function maxSubarraySum(nums: number[]): number { let best = nums[0]; let curr = nums[0]; for (let i = 1; i < nums.length; i++) { curr = Math.max(nums[i], curr + nums[i]); best = Math.max(best, curr); } return best; } ``` ## Python ```python def max_subarray_sum(nums): best = curr = nums[0] for x in nums[1:]: curr = max(x, curr + x) best = max(best, curr) return best ``` ## PHP ```php function maxSubarraySum(array $nums): int { $best = $curr = $nums[0]; for ($i = 1; $i < count($nums); $i++) { $curr = max($nums[$i], $curr + $nums[$i]); $best = max($best, $curr); } return $best; } ``` ## Java ```java public static int maxSubarraySum(int[] nums) { int best = nums[0]; int curr = nums[0]; for (int i = 1; i < nums.length; i++) { curr = Math.max(nums[i], curr + nums[i]); best = Math.max(best, curr); } return best; } ``` ## C# ```csharp public static int MaxSubarraySum(int[] nums) { int best = nums[0]; int curr = nums[0]; for (int i = 1; i < nums.Length; i++) { curr = Math.Max(nums[i], curr + nums[i]); best = Math.Max(best, curr); } return best; } ``` --- title: "DP над подпоследовательностями (LIS / LCS / расстояние редактирования)" url: https://algopath.pro/ru/patterns/dp-subsequence language: ru summary: "Для одной последовательности dp[i] хранит самую длинную цепочку, которая кончается на i. Для двух ячейка i и j отвечает за пару префиксов." updated: 2026-10-17 --- # DP над подпоследовательностями (LIS / LCS / расстояние редактирования) Для одной последовательности dp[i] хранит самую длинную цепочку, которая кончается на i. Для двух ячейка i и j отвечает за пару префиксов. ## DP над подпоследовательностями (LIS / LCS / расстояние редактирования): как это работает? Для одной последовательности dp[i] это самая длинная возрастающая цепочка, которая кончается на элементе i. Каждое dp[i] начинается с 1: элемент сам по себе. Для каждого i просмотрите все предыдущие j. Если arr[j] меньше, цепочка на j может взять arr[i] следующим. Тогда dp[i] становится большим из самого себя и dp[j] плюс один. Ответ это наибольшее dp[i], а не последнее. Для двух последовательностей строят таблицу. Ячейка i и j это ответ для этих двух префиксов. Если текущие элементы совпали, прибавьте единицу к ячейке слева сверху. Если нет, возьмите лучшую из двух соседних ячеек. Нулевая строка и нулевой столбец означают пустые префиксы. Каждая строка читает только строку над собой, поэтому хватает одной строки памяти. Источники: [Википедия: Наибольшая общая подпоследовательность](https://ru.wikipedia.org/wiki/%D0%9D%D0%B0%D0%B8%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B0%D1%8F_%D0%BE%D0%B1%D1%89%D0%B0%D1%8F_%D0%BF%D0%BE%D0%B4%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D1%8C), [Википедия: Расстояние Левенштейна](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D1%81%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5_%D0%9B%D0%B5%D0%B2%D0%B5%D0%BD%D1%88%D1%82%D0%B5%D0%B9%D0%BD%D0%B0), [Wagner, Fischer (1974), The string-to-string correction problem (англ.)](https://doi.org/10.1145/321796.321811) - `arr = [2, 5, 3, 7, 4, 8], все dp = 1` Каждый элемент сам по себе это цепочка длины 1. - `dp[1] = 2, dp[2] = 2` И 5, и 3 могут идти после 2. Других меньших элементов перед ними нет. - `dp[3] = 3` 7 может идти после 5 или 3, у обоих dp равно 2. Прибавляем один. - `dp[4] = 3` 4 может идти после 3, но не после 5 или 7. Значит оно продолжает 2, 3. - `dp = [1, 2, 2, 3, 3, 4], ответ = 4` 8 может идти после любого элемента. Одна из самых длинных цепочек это 2, 5, 7, 8. ## DP над подпоследовательностями (LIS / LCS / расстояние редактирования): когда применять? Паттерн «DP над подпоследовательностями (LIS / LCS / расстояние редактирования)» нужен, когда в условии есть слова «самая длинная возрастающая подпоследовательность», «самая длинная общая подпоследовательность двух строк» или «минимум правок, чтобы превратить одну строку в другую». Решение на нём обычно укладывается в O(n^2)/O(nm). - самая длинная возрастающая подпоследовательность - самая длинная общая подпоследовательность двух строк - минимум правок, чтобы превратить одну строку в другую - сохранить относительный порядок, можно пропускать элементы - diff или выравнивание двух последовательностей ## DP над подпоследовательностями (LIS / LCS / расстояние редактирования): с чем путают? Паттерн «DP над подпоследовательностями (LIS / LCS / расстояние редактирования)» чаще всего путают с этими паттернами: Динамическое программирование (1-D), DP на строках (word break), Бинарный поиск (массив) и Два указателя (в одну сторону). - **Динамическое программирование (1-D)** - Там хватает одного индекса. Сравнению двух последовательностей нужны два. - **DP на строках (word break)** - Та страница режет одну строку на куски. Эта выравнивает две последовательности. - **Бинарный поиск (массив)** - Версия возрастающей подпоследовательности за n log n использует бинарный поиск. Табличная версия медленнее. - **Два указателя (в одну сторону)** - Проверке, является ли строка подпоследовательностью, таблица не нужна. Поиску наибольшей общей нужна. ## DP над подпоследовательностями (LIS / LCS / расстояние редактирования): сложность по времени и памяти У паттерна «DP над подпоследовательностями (LIS / LCS / расстояние редактирования)» целевая сложность O(n^2)/O(nm). Одна последовательность из n элементов стоит O(n²) времени. Две длины n и m стоят O(nm), и хватает одной строки памяти. ## DP над подпоследовательностями (LIS / LCS / расстояние редактирования): разбор примера ### Расстояние редактирования между словами Верните наименьшее число посимвольных правок, превращающих одно слово в другое. Правка вставляет, удаляет или заменяет ровно один символ. Ячейка i и j это стоимость превращения первых i букв в первые j. При совпадении стоимость берётся с диагонали. При расхождении это единица плюс дешёвый сосед. ```javascript function minDistance(a, b) { // row 0 and column 0 hold the cost of deleting a whole prefix const dp = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)) ); for (let i = 1; i <= a.length; i++) { for (let j = 1; j <= b.length; j++) { if (a[i - 1] === b[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; // no edit needed } else { // replace, delete, insert dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]); } } } return dp[a.length][b.length]; } ``` ## DP над подпоследовательностями (LIS / LCS / расстояние редактирования): частые ошибки - **Путают подпоследовательность и подстроку** Подпоследовательность может пропускать элементы, подстрока нет. Переход у них разный. - **Ошибаются на единицу между таблицей и строкой** Ячейка i указывает на символ с индексом i минус один. Их смешение сдвигает все сравнения. - **Заполняют базовую строку нулями** Расстояние редактирования начинается со стоимости удаления целого префикса. Нули там дают неверный ответ. - **Берут таблицу при большом n** O(n в квадрате) умирает на 1e5 элементов. У возрастающей подпоследовательности есть форма за n log n. ## DP над подпоследовательностями (LIS / LCS / расстояние редактирования): задачи с собеседований Паттерн «DP над подпоследовательностями (LIS / LCS / расстояние редактирования)» решает такие задачи с собеседований: Наибольшая общая подпоследовательность, Расстояние редактирования, Наибольшая возрастающая подпоследовательность, Удаления для двух строк, Число различных подпоследовательностей, Чередование строк и Кратчайшая общая надпоследовательность. - **Наибольшая общая подпоследовательность** Чистая форма для двух последовательностей. - **Расстояние редактирования** Выбор идёт из трёх соседей, а не из двух. - **Наибольшая возрастающая подпоследовательность** Одна последовательность, сравниваемая сама с собой. - **Удаления для двух строк** Общая длина минус удвоенная общая подпоследовательность. - **Число различных подпоследовательностей** Считается количество способов, а не длина. - **Чередование строк** Два источника, питающие одну целевую строку. - **Кратчайшая общая надпоследовательность** Ответ восстанавливают, идя по таблице назад. ## JavaScript ```javascript function longestIncreasingSubsequence(arr) { const dp = new Array(arr.length).fill(1); let best = arr.length ? 1 : 0; for (let i = 1; i < arr.length; i++) { for (let j = 0; j < i; j++) { if (arr[j] < arr[i]) dp[i] = Math.max(dp[i], dp[j] + 1); } best = Math.max(best, dp[i]); } return best; } ``` ## TypeScript ```typescript function longestIncreasingSubsequence(arr: number[]): number { const dp = new Array(arr.length).fill(1); let best = arr.length ? 1 : 0; for (let i = 1; i < arr.length; i++) { for (let j = 0; j < i; j++) { if (arr[j] < arr[i]) dp[i] = Math.max(dp[i], dp[j] + 1); } best = Math.max(best, dp[i]); } return best; } ``` ## Python ```python def longest_increasing_subsequence(arr): if not arr: return 0 dp = [1] * len(arr) for i in range(1, len(arr)): for j in range(i): if arr[j] < arr[i]: dp[i] = max(dp[i], dp[j] + 1) return max(dp) ``` ## PHP ```php function longestIncreasingSubsequence(array $arr): int { $n = count($arr); if ($n === 0) return 0; $dp = array_fill(0, $n, 1); for ($i = 1; $i < $n; $i++) { for ($j = 0; $j < $i; $j++) { if ($arr[$j] < $arr[$i]) { $dp[$i] = max($dp[$i], $dp[$j] + 1); } } } return max($dp); } ``` ## Java ```java public static int longestIncreasingSubsequence(int[] arr) { int[] dp = new int[arr.length]; Arrays.fill(dp, 1); int best = arr.length > 0 ? 1 : 0; for (int i = 1; i < arr.length; i++) { for (int j = 0; j < i; j++) { if (arr[j] < arr[i]) dp[i] = Math.max(dp[i], dp[j] + 1); } best = Math.max(best, dp[i]); } return best; } ``` ## C# ```csharp public static int LongestIncreasingSubsequence(int[] arr) { var dp = new int[arr.Length]; Array.Fill(dp, 1); int best = arr.Length > 0 ? 1 : 0; for (int i = 1; i < arr.Length; i++) { for (int j = 0; j < i; j++) { if (arr[j] < arr[i]) dp[i] = Math.Max(dp[i], dp[j] + 1); } best = Math.Max(best, dp[i]); } return best; } ``` --- title: "DP на строках (word break)" url: https://algopath.pro/ru/patterns/dp-strings language: ru summary: "Режьте строку в каждой позиции и спрашивайте, годится ли получившийся кусок. Таблица достижимых позиций убирает повторы." updated: 2026-10-17 --- # DP на строках (word break) Режьте строку в каждой позиции и спрашивайте, годится ли получившийся кусок. Таблица достижимых позиций убирает повторы. ## DP на строках (word break): как это работает? Состояние i значит, что первые i символов можно разрезать. Нулевое состояние истинно по определению. Для каждой позиции i смотрите назад на все более ранние позиции j. Кусок между ними это кандидат в слово. Разрез работает, если состояние j истинно и кусок это слово. Тогда истинно и состояние i. Одного подходящего j достаточно. Прерывайте внутренний цикл сразу после находки. Ответ лежит в последнем состоянии. Дальше позиции n ничего нет. Каждая позиция решается ровно один раз. Именно это превращает экспоненту в таблицу. Источники: [Википедия: Динамическое программирование](https://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) - `dp[0] = истина` Режем leetcode словами leet и code. - `dp[1] по dp[3] = ложь` Куски l, le и lee словами не являются. - `dp[4] = истина` leet это слово, а dp[0] истинно. - `dp[5] по dp[7] = ложь` Ни одно слово не кончается в этих позициях. - `dp[8] = истина` code идёт после dp[4]. Вся строка режется. ## DP на строках (word break): когда применять? Паттерн «DP на строках (word break)» нужен, когда в условии есть слова «можно ли разбить строку на слова из словаря», «сегментировать строку по списку слов» или «булево значение достижимости в каждой точке разреза». Решение на нём обычно укладывается в O(n^2). - можно ли разбить строку на слова из словаря - сегментировать строку по списку слов - булево значение достижимости в каждой точке разреза - слитный текст нужно разделить на границы слов ## DP на строках (word break): с чем путают? Паттерн «DP на строках (word break)» чаще всего путают с этими паттернами: DP над подпоследовательностями (LIS / LCS / расстояние редактирования), Trie (префиксное дерево), Бэктрекинг и Хеш-множество / словарь. - **DP над подпоследовательностями (LIS / LCS / расстояние редактирования)** - Там выравниваются две разные последовательности. Здесь режется одна строка. - **Trie (префиксное дерево)** - Trie ускоряет поиск по словарю. Логика разрезания над ним не меняется. - **Бэктрекинг** - Бэктрекинг перечисляет все верные разрезы. Таблица отвечает лишь, есть ли хоть один. - **Хеш-множество / словарь** - Словарь слов обычно и есть множество. Таблица нужна, чтобы не проверять суффикс заново. ## DP на строках (word break): сложность по времени и памяти У паттерна «DP на строках (word break)» целевая сложность O(n^2). n символов при словах до m дают O(nm). Множество или trie держат поиск около O(1). ## DP на строках (word break): разбор примера ### Наименьшее число разрезов на палиндромы Разрежьте строку на куски, каждый из которых палиндром. Верните наименьшее число разрезов. Один символ уже считается палиндромом. Сначала отметьте все пары позиций, между которыми стоит палиндром. Потом состояние i это наименьшее число разрезов для первых i символов. Палиндромный хвост стоит одного разреза. ```javascript function minCut(s) { const n = s.length; const isPal = Array.from({ length: n }, () => new Array(n).fill(false)); for (let end = 0; end < n; end++) { for (let start = end; start >= 0; start--) { // the inside is already decided, because end - start only shrinks if (s[start] === s[end] && (end - start < 2 || isPal[start + 1][end - 1])) { isPal[start][end] = true; } } } const cuts = new Array(n + 1).fill(0); for (let i = 0; i <= n; i++) cuts[i] = i - 1; // cuts[0] is -1, so a whole palindrome costs 0 for (let end = 0; end < n; end++) { for (let start = 0; start <= end; start++) { if (isPal[start][end]) { cuts[end + 1] = Math.min(cuts[end + 1], cuts[start] + 1); } } } return cuts[n]; } ``` ## DP на строках (word break): частые ошибки - **Пересобирают подстроку на каждом шаге** Вырезание внутри цикла каждый раз копирует символы. Сравнивайте позиции или идите по trie. - **Идут по палиндромам не в ту сторону** Внутренность обязана решаться раньше внешней. Растите по длине или ведите начало назад. - **Забывают пустой префикс** Нулевое состояние и позволяет первому слову начать разрез. Без него истины не будет никогда. - **Перечисляют все разрезы, когда хватит одного** Задача спрашивает только да или нет. Перечисление разрезов экспоненциально. ## DP на строках (word break): задачи с собеседований Паттерн «DP на строках (word break)» решает такие задачи с собеседований: Разбиение строки на слова, Разбиение строки на слова II, Разбиение на палиндромы II, Наибольшая палиндромная подстрока, Составные слова, Лишние символы в строке и Число способов расшифровки. - **Разбиение строки на слова** Можно ли вообще разрезать строку. - **Разбиение строки на слова II** Нужны все верные разрезы, поэтому возвращается бэктрекинг. - **Разбиение на палиндромы II** Нужно число разрезов, а не сами части. - **Наибольшая палиндромная подстрока** Та же таблица палиндромов, прочитанная иначе. - **Составные слова** Каждое слово режется при помощи всех остальных. - **Лишние символы в строке** Считается стоимость оставшихся букв. - **Число способов расшифровки** Одна цифра или две, по фиксированному алфавиту. ## JavaScript ```javascript function wordBreak(s, wordDict) { const words = new Set(wordDict); const ok = new Array(s.length + 1).fill(false); ok[0] = true; for (let i = 1; i <= s.length; i++) { for (let j = 0; j < i; j++) { if (ok[j] && words.has(s.slice(j, i))) { ok[i] = true; break; } } } return ok[s.length]; } ``` ## TypeScript ```typescript function wordBreak(s: string, wordDict: string[]): boolean { const words = new Set(wordDict); const ok = new Array(s.length + 1).fill(false); ok[0] = true; for (let i = 1; i <= s.length; i++) { for (let j = 0; j < i; j++) { if (ok[j] && words.has(s.slice(j, i))) { ok[i] = true; break; } } } return ok[s.length]; } ``` ## Python ```python def word_break(s, word_dict): words = set(word_dict) ok = [False] * (len(s) + 1) ok[0] = True for i in range(1, len(s) + 1): for j in range(i): if ok[j] and s[j:i] in words: ok[i] = True break return ok[len(s)] ``` ## PHP ```php function wordBreak(string $s, array $wordDict): bool { $words = array_flip($wordDict); $n = strlen($s); $ok = array_fill(0, $n + 1, false); $ok[0] = true; for ($i = 1; $i <= $n; $i++) { for ($j = 0; $j < $i; $j++) { if ($ok[$j] && isset($words[substr($s, $j, $i - $j)])) { $ok[$i] = true; break; } } } return $ok[$n]; } ``` ## Java ```java public static boolean wordBreak(String s, String[] wordDict) { Set words = new HashSet<>(Arrays.asList(wordDict)); boolean[] ok = new boolean[s.length() + 1]; ok[0] = true; for (int i = 1; i <= s.length(); i++) { for (int j = 0; j < i; j++) { if (ok[j] && words.contains(s.substring(j, i))) { ok[i] = true; break; } } } return ok[s.length()]; } ``` ## C# ```csharp public static bool WordBreak(string s, string[] wordDict) { var words = new HashSet(wordDict); var ok = new bool[s.Length + 1]; ok[0] = true; for (int i = 1; i <= s.Length; i++) { for (int j = 0; j < i; j++) { if (ok[j] && words.Contains(s.Substring(j, i - j))) { ok[i] = true; break; } } } return ok[s.Length]; } ``` --- title: "DP на интервалах" url: https://algopath.pro/ru/patterns/dp-interval language: ru summary: "Состояние здесь это отрезок, а переход выбирает последний ход внутри него. Короткие отрезки всегда заполняются раньше длинных." updated: 2026-10-17 --- # DP на интервалах Состояние здесь это отрезок, а переход выбирает последний ход внутри него. Короткие отрезки всегда заполняются раньше длинных. ## DP на интервалах: как это работает? Состояние это пара, два конца отрезка. Оно хранит лучший результат внутри этих границ. Идите по длине отрезка, от коротких к длинным. Всё, что вы читаете, уже посчитано. Для одного отрезка переберите все точки разреза внутри. Каждый разрез даёт одного кандидата. Кандидат складывает левую часть, правую и стоимость их соединения. Оставляйте лучшего из них. Вся задача сидит именно в стоимости соединения. В шариках она зависит от двух границ. Ответ это состояние, накрывающее весь вход. Оно заполняется самым последним. Источники: [Википедия: Задача о порядке перемножения матриц](https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%B4%D0%B0%D1%87%D0%B0_%D0%BE_%D0%BF%D0%BE%D1%80%D1%8F%D0%B4%D0%BA%D0%B5_%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F_%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86) - `с полями: [1, 3, 1, 5, 1]` Лопаем шарики. По краям добавлена единица, она не лопается. - `отрезки ширины один: 0` Внутри них лопать нечего. - `dp(1, 3) = 15` Единица между 3 и 5 даёт 3 умножить на 1 и на 5. - `dp(0, 3) = 30` Тройку лопаем последней, поэтому она стоит 1 на 3 на 5. - `dp(0, 4) = 35` Пятёрка идёт последней. Все короткие отрезки уже готовы. ## DP на интервалах: когда применять? Паттерн «DP на интервалах» нужен, когда в условии есть слова «объединить соседние части с некоторой стоимостью», «лучший способ разбить отрезок на две части» или «порядок перемножения цепочки матриц». Решение на нём обычно укладывается в O(n^3). - объединить соседние части с некоторой стоимостью - лучший способ разбить отрезок на две части - порядок перемножения цепочки матриц - стоимость слияния зависит от всего отрезка - оптимальная расстановка скобок или порядок построения ## DP на интервалах: с чем путают? Паттерн «DP на интервалах» чаще всего путают с этими паттернами: Динамическое программирование (1-D), DP над подпоследовательностями (LIS / LCS / расстояние редактирования), DP на деревьях и Жадный алгоритм (обменный аргумент). - **Динамическое программирование (1-D)** - Один индекс описывает префикс. Отрезку нужны оба конца. - **DP над подпоследовательностями (LIS / LCS / расстояние редактирования)** - Там два индекса указывают в две разные последовательности. Здесь оба лежат в одной. - **DP на деревьях** - Дерево делится по своим потомкам, и они заданы. Отрезок делится в точке, которую вы выбираете. - **Жадный алгоритм (обменный аргумент)** - Жадность выбрала бы порядок и на нём остановилась. Здесь надо перебрать все точки разреза. ## DP на интервалах: сложность по времени и памяти У паттерна «DP на интервалах» целевая сложность O(n^3). n примерно до 500 даёт O(n в кубе). Отрезков n в квадрате, и у каждого n точек разреза. ## DP на интервалах: разбор примера ### Наибольшая палиндромная подпоследовательность Верните длину наибольшей палиндромной подпоследовательности строки. Буквы не обязаны стоять рядом друг с другом. Состоянием служит отрезок строки. Если концы совпали, они добавляют двойку к внутреннему отрезку. Иначе отбросьте один конец и возьмите лучшее. ```javascript function longestPalindromeSubseq(s) { const n = s.length; const dp = Array.from({ length: n }, () => new Array(n).fill(0)); // i walks backwards so every inner range is already filled for (let i = n - 1; i >= 0; i--) { dp[i][i] = 1; // a single letter is a palindrome for (let j = i + 1; j < n; j++) { if (s[i] === s[j]) { dp[i][j] = dp[i + 1][j - 1] + 2; } else { dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); } } } return dp[0][n - 1]; } ``` ## DP на интервалах: частые ошибки - **Гоняют i и j как в обычной сетке** Отрезок заполняется после всего, что внутри него. Идите по длине или ведите i назад. - **Выбирают первый ход вместо последнего** В шариках фиксированных соседей имеет только последний лопнувший. Выбор первого делает состояние неверным. - **Забывают про поля по краям** Нейтральное значение с каждой стороны убирает крайние случаи. Без него границы требуют отдельного кода. - **Запускают это на большом входе** O(n в кубе) умирает после нескольких тысяч элементов. Проверьте n до написания трёх циклов. ## DP на интервалах: задачи с собеседований Паттерн «DP на интервалах» решает такие задачи с собеседований: Лопающиеся шарики, Наибольшая палиндромная подпоследовательность, Минимальная триангуляция многоугольника, Странный принтер, Удаление коробок, Число палиндромных подстрок и Минимальная стоимость слияния камней. - **Лопающиеся шарики** Выбирается шарик, который лопнет последним. - **Наибольшая палиндромная подпоследовательность** Концы совпали или один из них отброшен. - **Минимальная триангуляция многоугольника** Каждая точка разреза даёт треугольник. - **Странный принтер** Одна печать накрывает целый отрезок. - **Удаление коробок** Состояние несёт ещё и счётчик рядом с отрезком. - **Число палиндромных подстрок** Считается каждый отрезок, который является палиндромом. - **Минимальная стоимость слияния камней** Деление идёт на группы по k, а не надвое. ## JavaScript ```javascript function minMergeCost(sizes) { const n = sizes.length; const prefix = [0]; for (const s of sizes) prefix.push(prefix[prefix.length - 1] + s); const dp = Array.from({ length: n }, () => new Array(n).fill(0)); for (let len = 2; len <= n; len++) { for (let i = 0; i + len - 1 < n; i++) { const j = i + len - 1; dp[i][j] = Infinity; for (let k = i; k < j; k++) { const cost = dp[i][k] + dp[k + 1][j] + (prefix[j + 1] - prefix[i]); dp[i][j] = Math.min(dp[i][j], cost); } } } return dp[0][n - 1]; } ``` ## TypeScript ```typescript function minMergeCost(sizes: number[]): number { const n = sizes.length; const prefix: number[] = [0]; for (const s of sizes) prefix.push(prefix[prefix.length - 1] + s); const dp: number[][] = Array.from({ length: n }, () => new Array(n).fill(0)); for (let len = 2; len <= n; len++) { for (let i = 0; i + len - 1 < n; i++) { const j = i + len - 1; dp[i][j] = Infinity; for (let k = i; k < j; k++) { const cost = dp[i][k] + dp[k + 1][j] + (prefix[j + 1] - prefix[i]); dp[i][j] = Math.min(dp[i][j], cost); } } } return dp[0][n - 1]; } ``` ## Python ```python def min_merge_cost(sizes): n = len(sizes) prefix = [0] for s in sizes: prefix.append(prefix[-1] + s) dp = [[0] * n for _ in range(n)] for length in range(2, n + 1): for i in range(n - length + 1): j = i + length - 1 dp[i][j] = min( dp[i][k] + dp[k + 1][j] + (prefix[j + 1] - prefix[i]) for k in range(i, j) ) return dp[0][n - 1] ``` ## PHP ```php function minMergeCost(array $sizes): int { $n = count($sizes); $prefix = [0]; foreach ($sizes as $s) $prefix[] = end($prefix) + $s; $dp = array_fill(0, $n, array_fill(0, $n, 0)); for ($len = 2; $len <= $n; $len++) { for ($i = 0; $i + $len - 1 < $n; $i++) { $j = $i + $len - 1; $dp[$i][$j] = PHP_INT_MAX; for ($k = $i; $k < $j; $k++) { $cost = $dp[$i][$k] + $dp[$k + 1][$j] + ($prefix[$j + 1] - $prefix[$i]); $dp[$i][$j] = min($dp[$i][$j], $cost); } } } return $dp[0][$n - 1]; } ``` ## Java ```java public static int minMergeCost(int[] sizes) { int n = sizes.length; int[] prefix = new int[n + 1]; for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + sizes[i]; int[][] dp = new int[n][n]; for (int len = 2; len <= n; len++) { for (int i = 0; i + len - 1 < n; i++) { int j = i + len - 1; dp[i][j] = Integer.MAX_VALUE; for (int k = i; k < j; k++) { int cost = dp[i][k] + dp[k + 1][j] + (prefix[j + 1] - prefix[i]); dp[i][j] = Math.min(dp[i][j], cost); } } } return dp[0][n - 1]; } ``` ## C# ```csharp public static int MinMergeCost(int[] sizes) { int n = sizes.Length; var prefix = new int[n + 1]; for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + sizes[i]; var dp = new int[n][]; for (int i = 0; i < n; i++) dp[i] = new int[n]; for (int len = 2; len <= n; len++) { for (int i = 0; i + len - 1 < n; i++) { int j = i + len - 1; dp[i][j] = int.MaxValue; for (int k = i; k < j; k++) { int cost = dp[i][k] + dp[k + 1][j] + (prefix[j + 1] - prefix[i]); dp[i][j] = Math.Min(dp[i][j], cost); } } } return dp[0][n - 1]; } ``` --- title: "DP на деревьях" url: https://algopath.pro/ru/patterns/dp-on-trees language: ru summary: "Ответ каждого узла собирается из ответов его собственных потомков. Один обратный обход по дереву считает их все за проход." updated: 2026-10-17 --- # DP на деревьях Ответ каждого узла собирается из ответов его собственных потомков. Один обратный обход по дереву считает их все за проход. ## DP на деревьях: как это работает? Решите, что один узел возвращает родителю. Часто это пара, по значению на каждый вариант. Сначала спуститесь во всех потомков. До их ответа ничего решить нельзя. Соберите ответ узла из ответов потомков. Эта сборка и есть переход. Верните результат наверх. Больше родитель о поддереве ничего не знает. Если ответ может лежать где угодно, ведите глобальный максимум при возврате. Одного корня не всегда хватает. Один обратный обход касается каждого узла один раз. В этом вся стоимость. Источники: [Википедия: Динамическое программирование](https://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5), [Википедия: Обход дерева](https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%85%D0%BE%D0%B4_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%B0) - `лист возвращает (3, 0)` Грабим дома на дереве. Взять лист даёт 3, пропустить даёт 0. - `узел 2 возвращает (2, 3)` Взять его и пропустить потомка либо пропустить и взять лучшее. - `узел 3 возвращает (3, 1)` Его собственный потомок стоит всего 1. - `корень взят = 3 + 3 + 1` Взятие корня заставляет пропустить обоих потомков. - `ответ = 7` Семёрка бьёт шестёрку, которую даёт пропуск корня. ## DP на деревьях: когда применять? Паттерн «DP на деревьях» нужен, когда в условии есть слова «лучший выбор по иерархии или оргструктуре», «нельзя выбрать узел вместе с его прямым родителем» или «постпорядок: дети решаются раньше родителя». Решение на нём обычно укладывается в O(n). - лучший выбор по иерархии или оргструктуре - нельзя выбрать узел вместе с его прямым родителем - постпорядок: дети решаются раньше родителя - каждый узел объединяет состояния своих детей ## DP на деревьях: с чем путают? Паттерн «DP на деревьях» чаще всего путают с этими паттернами: Обход дерева, Динамическое программирование (1-D), Рекурсия с мемоизацией и Обход графа BFS / DFS. - **Обход дерева** - Та страница про порядок посещения. Эта про то, что уносится наверх. - **Динамическое программирование (1-D)** - У прямой одно направление, поэтому хватает индекса. Здесь порядок задаёт структура дерева. - **Рекурсия с мемоизацией** - В дереве подзадачи не повторяются, поэтому кеш не нужен. Каждый узел посещается один раз. - **Обход графа BFS / DFS** - В обычный граф можно попасть в узел дважды, и рекурсия может не кончиться. В дереве нельзя. ## DP на деревьях: сложность по времени и памяти У паттерна «DP на деревьях» целевая сложность O(n). n узлов дают O(n), если в узле работы на O(1). Несколько состояний на узел это умножают. ## DP на деревьях: разбор примера ### Наибольшая сумма по любому пути Верните наибольшую сумму по любому пути в двоичном дереве. Путь может начинаться и кончаться где угодно, но не может ветвиться дважды. Каждый узел возвращает лучший прямой путь, идущий вниз через него. Отрицательная ветка отбрасывается. Лучший разветвлённый путь пишется в глобальный максимум при возврате. ```javascript function maxPathSum(root) { let best = -Infinity; function down(node) { if (!node) return 0; // a negative branch is worse than taking nothing at all const left = Math.max(0, down(node.left)); const right = Math.max(0, down(node.right)); best = Math.max(best, node.val + left + right); // the fork stays local return node.val + Math.max(left, right); // the parent can only use one side } down(root); return best; } ``` ## DP на деревьях: частые ошибки - **Возвращают то же, что и записывают** Наверх уходит прямой путь, а лучший ответ может ветвиться. Держите их раздельно. - **Считают до ответа потомков** Переходу сначала нужны оба потомка. Сначала спуск, потом сборка. - **Не разбирают случай пустого потомка** Узел с одним потомком всё равно вызывает вторую сторону. Верните там нейтральное значение. - **Начинают максимум с нуля** Старт с нуля прячет дерево, где все значения отрицательные. Начинайте с минус бесконечности. ## DP на деревьях: задачи с собеседований Паттерн «DP на деревьях» решает такие задачи с собеседований: Ограбление домов III, Максимальная сумма пути в дереве, Диаметр двоичного дерева, Самый длинный путь из равных значений, Распределение монет в дереве, Число хороших узлов и Наклон дерева. - **Ограбление домов III** Два состояния на узел: взят или пропущен. - **Максимальная сумма пути в дереве** Возвращается прямой путь, записывается разветвлённый. - **Диаметр двоичного дерева** Глубина слева плюс глубина справа в каждом узле. - **Самый длинный путь из равных значений** Продолжать можно только через равных потомков. - **Распределение монет в дереве** Излишек передаётся наверх родителю. - **Число хороших узлов** Максимум несут вниз, а не наверх. - **Наклон дерева** Разность сумм двух поддеревьев. ## JavaScript ```javascript function maxNonAdjacent(node) { if (!node) return [0, 0]; // [incl, excl] let inclSum = node.value; let exclSum = 0; for (const child of node.children) { const [inclC, exclC] = maxNonAdjacent(child); inclSum += exclC; exclSum += Math.max(inclC, exclC); } return [inclSum, exclSum]; } ``` ## TypeScript ```typescript interface TreeNode { value: number; children: TreeNode[]; } function maxNonAdjacent(node: TreeNode | null): [number, number] { if (!node) return [0, 0]; // [incl, excl] let inclSum = node.value; let exclSum = 0; for (const child of node.children) { const [inclC, exclC] = maxNonAdjacent(child); inclSum += exclC; exclSum += Math.max(inclC, exclC); } return [inclSum, exclSum]; } ``` ## Python ```python def max_non_adjacent(node): if node is None: return (0, 0) # (incl, excl) incl_sum = node.value excl_sum = 0 for child in node.children: incl_c, excl_c = max_non_adjacent(child) incl_sum += excl_c excl_sum += max(incl_c, excl_c) return (incl_sum, excl_sum) ``` ## PHP ```php function maxNonAdjacent(?Node $node): array { if ($node === null) return [0, 0]; // [incl, excl] $inclSum = $node->value; $exclSum = 0; foreach ($node->children as $child) { [$inclC, $exclC] = maxNonAdjacent($child); $inclSum += $exclC; $exclSum += max($inclC, $exclC); } return [$inclSum, $exclSum]; } ``` ## Java ```java // returns {incl, excl}: the best sum that takes this node, and the best that skips it public static int[] maxNonAdjacent(TreeNode node) { if (node == null) return new int[] { 0, 0 }; int inclSum = node.value; int exclSum = 0; for (TreeNode child : node.children) { int[] best = maxNonAdjacent(child); inclSum += best[1]; exclSum += Math.max(best[0], best[1]); } return new int[] { inclSum, exclSum }; } public static class TreeNode { public int value; public List children = new ArrayList<>(); } ``` ## C# ```csharp // returns {incl, excl}: the best sum that takes this node, and the best that skips it public static int[] MaxNonAdjacent(TreeNode node) { if (node == null) return new int[] { 0, 0 }; int inclSum = node.value; int exclSum = 0; foreach (var child in node.children) { int[] best = MaxNonAdjacent(child); inclSum += best[1]; exclSum += Math.Max(best[0], best[1]); } return new int[] { inclSum, exclSum }; } public class TreeNode { public int value; public List children = new List(); } ``` --- title: "Trie (префиксное дерево)" url: https://algopath.pro/ru/patterns/trie language: ru summary: "Слова с общим префиксом делят между собой один и тот же путь от корня. Поиск стоит ровно столько, какова длина этого слова." updated: 2026-10-17 --- # Trie (префиксное дерево) Слова с общим префиксом делят между собой один и тот же путь от корня. Поиск стоит ровно столько, какова длина этого слова. ## Trie (префиксное дерево): как это работает? Корень означает пустой префикс. Своего символа он не несёт. Каждое ребро хранит один символ. Путь от корня складывается в префикс. Вставка идёт по слову символ за символом. Отсутствующий потомок создаётся по дороге. Последний узел слова получает флаг. Без него префикс выглядел бы как сохранённое слово. Поиск идёт тем же путём вниз. Отсутствующий потомок означает, что слова нет. Запрос по префиксу останавливается раньше и сообщает успех. Всё под этим узлом начинается с префикса. Источники: [Википедия: Префиксное дерево](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D1%84%D0%B8%D0%BA%D1%81%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE), [Fredkin (1960), Trie memory (англ.)](https://doi.org/10.1145/367390.367400) - `корень в c` Вставляем car и cat. Первый символ создаёт одного потомка. - `c, a, r с флагом` Три узла и один флаг конца складываются в car. - `вставка cat: c и a уже есть` Общий префикс проходится, а не строится заново. - `a в t с флагом` На второе слово добавляется всего один узел. - `поиск ca: флага нет` Узел существует, но словом не является. Это только префикс. ## Trie (префиксное дерево): когда применять? Паттерн «Trie (префиксное дерево)» нужен, когда в условии есть слова «автодополнение или typeahead по множеству строк», «самый длинный общий префикс среди слов» или «вставка, поиск и проверка префикса в наборе слов». Решение на нём обычно укладывается в O(len) per op. - автодополнение или typeahead по множеству строк - самый длинный общий префикс среди слов - вставка, поиск и проверка префикса в наборе слов - сопоставление с большим фиксированным словарём по префиксу ## Trie (префиксное дерево): с чем путают? Паттерн «Trie (префиксное дерево)» чаще всего путают с этими паттернами: Хеш-множество / словарь, Двоичное дерево поиска, Поиск слова в матрице (DFS/backtracking по сетке) и DP на строках (word break). - **Хеш-множество / словарь** - Словарь сравнивает только целые ключи. Trie отвечает ещё и на запросы по префиксу. - **Двоичное дерево поиска** - Дерево поиска сравнивает целые ключи и выбирает сторону. Trie идёт по одному символу. - **Поиск слова в матрице (DFS/backtracking по сетке)** - Поиск многих слов в сетке использует trie для обрезки. Та страница про сам обход сетки. - **DP на строках (word break)** - Разбиение на слова использует trie для проверки куска. Решение о разрезе лежит в таблице. ## Trie (префиксное дерево): сложность по времени и памяти У паттерна «Trie (префиксное дерево)» целевая сложность O(len) per op. Слово длины m стоит O(m) на вставку или поиск. Память O(суммы символов) по всем словам. ## Trie (префиксное дерево): разбор примера ### Замена слов их корнями Дан словарь корней и предложение. Замените каждое слово самым коротким корнем, с которого оно начинается. Слово без подходящего корня остаётся как есть. Сначала вставьте все корни в trie. Потом ведите каждое слово вниз по дереву. Останавливайтесь на первом флаге конца, это и есть кратчайший корень. ```javascript function replaceWords(dictionary, sentence) { const root = { children: {} }; for (const word of dictionary) { let node = root; for (const c of word) { node.children[c] = node.children[c] ?? { children: {} }; node = node.children[c]; } node.end = true; // this node closes a real word } return sentence .split(" ") .map((word) => { let node = root; let prefix = ""; for (const c of word) { if (!node.children[c]) return word; // no root matches node = node.children[c]; prefix += c; if (node.end) return prefix; // the first flag is the shortest root } return word; }) .join(" "); } ``` ## Trie (префиксное дерево): частые ошибки - **Не ставят флаг конца** Тогда сохранённый префикс не отличить от сохранённого слова. Любой поиск превращается в запрос по префиксу. - **Смешивают флаг с потомками** Флаг под ключом, похожим на букву, портит дерево. Держите потомков в отдельном словаре. - **Строят его ради горстки слов** Trie тратит память на каждый символ. До нескольких тысяч слов проще множество. - **Считают алфавит фиксированным** Массив на 26 ячеек ломается на цифрах и диакритике. Берите словарь, если вход не из обычных букв. ## Trie (префиксное дерево): задачи с собеседований Паттерн «Trie (префиксное дерево)» решает такие задачи с собеседований: Реализация trie, Замена слов корнями, Добавление и поиск слов, Поиск слов в сетке II, Наибольший общий префикс, Максимальный XOR двух чисел и Система автодополнения. - **Реализация trie** Вставка, поиск и запрос по префиксу. - **Замена слов корнями** Обход останавливается на первом флаге конца. - **Добавление и поиск слов** Точка означает перебор всех потомков на уровне. - **Поиск слов в сетке II** Trie заранее обрезает обход сетки. - **Наибольший общий префикс** Спускайтесь, пока потомок ровно один. - **Максимальный XOR двух чисел** Двоичный trie, построенный по битам. - **Система автодополнения** Ранжируйте слова, лежащие под узлом префикса. ## JavaScript ```javascript class TrieNode { constructor() { this.children = new Map(); this.isEnd = false; } } class Trie { constructor() { this.root = new TrieNode(); } insert(word) { let node = this.root; for (const c of word) { if (!node.children.has(c)) node.children.set(c, new TrieNode()); node = node.children.get(c); } node.isEnd = true; } search(word) { let node = this.root; for (const c of word) { if (!node.children.has(c)) return false; node = node.children.get(c); } return node.isEnd; } } ``` ## TypeScript ```typescript class TrieNode { children = new Map(); isEnd = false; } class Trie { private root = new TrieNode(); insert(word: string): void { let node = this.root; for (const c of word) { let next = node.children.get(c); if (!next) { next = new TrieNode(); node.children.set(c, next); } node = next; } node.isEnd = true; } search(word: string): boolean { let node = this.root; for (const c of word) { const next = node.children.get(c); if (!next) return false; node = next; } return node.isEnd; } } ``` ## Python ```python class TrieNode: def __init__(self): self.children = {} self.is_end = False class Trie: def __init__(self): self.root = TrieNode() def insert(self, word): node = self.root for c in word: node = node.children.setdefault(c, TrieNode()) node.is_end = True def search(self, word): node = self.root for c in word: if c not in node.children: return False node = node.children[c] return node.is_end ``` ## PHP ```php class TrieNode { public array $children = []; public bool $isEnd = false; } class Trie { private TrieNode $root; public function __construct() { $this->root = new TrieNode(); } public function insert(string $word): void { $node = $this->root; foreach (str_split($word) as $c) { $node->children[$c] ??= new TrieNode(); $node = $node->children[$c]; } $node->isEnd = true; } public function search(string $word): bool { $node = $this->root; foreach (str_split($word) as $c) { if (!isset($node->children[$c])) return false; $node = $node->children[$c]; } return $node->isEnd; } } ``` ## Java ```java public static class TrieNode { public Map children = new HashMap<>(); public boolean isEnd = false; } public static class Trie { private final TrieNode root = new TrieNode(); public void insert(String word) { TrieNode node = root; for (char c : word.toCharArray()) { node = node.children.computeIfAbsent(c, k -> new TrieNode()); } node.isEnd = true; } public boolean search(String word) { TrieNode node = root; for (char c : word.toCharArray()) { node = node.children.get(c); if (node == null) return false; } return node.isEnd; } } ``` ## C# ```csharp public class TrieNode { public Dictionary children = new Dictionary(); public bool isEnd = false; } public class Trie { private readonly TrieNode root = new TrieNode(); public void Insert(string word) { var node = root; foreach (char c in word) { if (!node.children.ContainsKey(c)) node.children[c] = new TrieNode(); node = node.children[c]; } node.isEnd = true; } public bool Search(string word) { var node = root; foreach (char c in word) { if (!node.children.TryGetValue(c, out var next)) return false; node = next; } return node.isEnd; } } ``` --- title: "Битовые манипуляции" url: https://algopath.pro/ru/patterns/bit-manipulation language: ru summary: "Считайте целое число длинным рядом переключателей. Маски, сдвиги и XOR читают или переключают каждый из них прямо на месте." updated: 2026-10-17 --- # Битовые манипуляции Считайте целое число длинным рядом переключателей. Маски, сдвиги и XOR читают или переключают каждый из них прямо на месте. ## Битовые манипуляции: как это работает? Число это ряд битов, каждый вдвое дороже соседа справа. Бит i стоит 2 в степени i. Сдвиг влево на один удваивает число. Сдвиг вправо делит пополам и теряет последний бит. AND с маской оставляет только те биты, что есть в маске. Так читают один бит. OR ставит бит, а XOR его переключает. Бит, сложенный по XOR сам с собой, даёт ноль. Этот факт и есть весь приём поиска одинокого значения. Каждая пара взаимно уничтожается. n AND n минус один снимает младший установленный бит. Повторение этого считает установленные биты. Источники: [Википедия: Битовая операция](https://ru.wikipedia.org/wiki/%D0%91%D0%B8%D1%82%D0%BE%D0%B2%D0%B0%D1%8F_%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F), [MDN (англ.): Bitwise AND (&)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_AND) - `n = 1100` Двенадцать в двоичной записи. У него два установленных бита. - `n & 1 = 0` Последний бит нулевой, значит число чётное. - `n >> 2 = 11` Два сдвига вправо оставляют тройку. - `n & (n - 1) = 1000` Вычитание единицы даёт 1011, и AND снимает младший установленный бит. - `два круга дают ноль` Значит у двенадцати ровно два установленных бита. ## Битовые манипуляции: когда применять? Паттерн «Битовые манипуляции» нужен, когда в условии есть слова «найти единственное или уникальное число через XOR», «установить, снять, переключить или проверить бит» или «упаковать много флагов да/нет в одно целое число». Решение на нём обычно укладывается в O(1)/O(n). - найти единственное или уникальное число через XOR - установить, снять, переключить или проверить бит - упаковать много флагов да/нет в одно целое число - подсчёт установленных битов или проверка степени двойки ## Битовые манипуляции: с чем путают? Паттерн «Битовые манипуляции» чаще всего путают с этими паттернами: Хеш-множество / словарь, Математика и теория чисел (НОД, решето, модульная арифметика), Бэктрекинг и Сортировка без сравнений (подсчётом / поразрядная). - **Хеш-множество / словарь** - Множество отвечает про принадлежность для любого ключа. Битовая маска умеет это для 32 известных элементов без памяти. - **Математика и теория чисел (НОД, решето, модульная арифметика)** - Оба работают с самим числом. Та страница про делители и простые, эта про биты. - **Бэктрекинг** - Подмножества можно перечислить простым счётом от нуля. Бэктрекинг строит тот же список рекурсией. - **Сортировка без сравнений (подсчётом / поразрядная)** - Поразрядная сортировка читает разряды, иногда как биты. Её цель группировка, а не переключение. ## Битовые манипуляции: сложность по времени и памяти У паттерна «Битовые манипуляции» целевая сложность O(1)/O(n). Одна операция стоит O(1) на 32-битном значении. Проход по битам это O(32), то есть константа. ## Битовые манипуляции: разбор примера ### Посчитать биты у всех чисел подряд Для каждого числа от 0 до n посчитайте, сколько у него установленных битов. Считать каждое отдельно можно, но это повторяет кучу работы. Снятие младшего установленного бита даёт меньшее число, уже посчитанное. Значит ответ для i это ответ для i AND i минус один плюс единица. ```javascript function countBits(n) { const bits = new Array(n + 1).fill(0); for (let i = 1; i <= n; i++) { // i & (i - 1) clears the lowest set bit, so it is always smaller bits[i] = bits[i & (i - 1)] + 1; } return bits; } ``` ## Битовые манипуляции: частые ошибки - **Сдвигают дальше 31 бита** Битовые операции JavaScript работают на 32 битах и заворачиваются. Дальше нужен BigInt. - **Забывают про знаковый бит** Результат сдвига может вернуться отрицательным. Для простого подсчёта берите беззнаковый сдвиг. - **Путают AND и OR** AND читает или снимает, а OR ставит. Их перестановка даёт молча неверную маску. - **Берут биты там, где важна ясность** Массив булевых значений читается лучше и работает так же быстро. Маска нужна, когда ограничение это память. ## Битовые манипуляции: задачи с собеседований Паттерн «Битовые манипуляции» решает такие задачи с собеседований: Одинокое число, Число единичных битов, Подсчёт битов, Пропущенное число, Подмножества, Степень двойки и Сумма двух целых. - **Одинокое число** Каждая пара сама себя уничтожает под XOR. - **Число единичных битов** Снимайте младший установленный бит, пока не станет ноль. - **Подсчёт битов** Каждый ответ переиспользует уже посчитанный меньший. - **Пропущенное число** Сложите по XOR индексы со значениями. - **Подмножества** Считайте от нуля до двойки в степени n. - **Степень двойки** Верно ровно тогда, когда n AND n минус один равно нулю. - **Сумма двух целых** XOR даёт сумму, а сдвинутый AND даёт перенос. ## JavaScript ```javascript function findUnique(nums) { return nums.reduce((acc, n) => acc ^ n, 0); } function setBit(mask, i) { return mask | (1 << i); } function clearBit(mask, i) { return mask & ~(1 << i); } function hasBit(mask, i) { return (mask & (1 << i)) !== 0; } ``` ## TypeScript ```typescript function findUnique(nums: number[]): number { return nums.reduce((acc, n) => acc ^ n, 0); } function setBit(mask: number, i: number): number { return mask | (1 << i); } function clearBit(mask: number, i: number): number { return mask & ~(1 << i); } function hasBit(mask: number, i: number): boolean { return (mask & (1 << i)) !== 0; } ``` ## Python ```python def find_unique(nums): acc = 0 for n in nums: acc ^= n return acc def set_bit(mask, i): return mask | (1 << i) def clear_bit(mask, i): return mask & ~(1 << i) def has_bit(mask, i): return (mask & (1 << i)) != 0 ``` ## PHP ```php function findUnique(array $nums): int { $acc = 0; foreach ($nums as $n) $acc ^= $n; return $acc; } function setBit(int $mask, int $i): int { return $mask | (1 << $i); } function clearBit(int $mask, int $i): int { return $mask & ~(1 << $i); } function hasBit(int $mask, int $i): bool { return ($mask & (1 << $i)) !== 0; } ``` ## Java ```java public static int findUnique(int[] nums) { int acc = 0; for (int n : nums) acc ^= n; return acc; } public static int setBit(int mask, int i) { return mask | (1 << i); } public static int clearBit(int mask, int i) { return mask & ~(1 << i); } public static boolean hasBit(int mask, int i) { return (mask & (1 << i)) != 0; } ``` ## C# ```csharp public static int FindUnique(int[] nums) { int acc = 0; foreach (int n in nums) acc ^= n; return acc; } public static int SetBit(int mask, int i) { return mask | (1 << i); } public static int ClearBit(int mask, int i) { return mask & ~(1 << i); } public static bool HasBit(int mask, int i) { return (mask & (1 << i)) != 0; } ``` --- title: "Математика и теория чисел (НОД, решето, модульная арифметика)" url: https://algopath.pro/ru/patterns/math-number-theory language: ru summary: "Теория чисел заменяет перебор всех значений обычной арифметикой. НОД, модульные правила и решето убирают полный перебор." updated: 2026-10-17 --- # Математика и теория чисел (НОД, решето, модульная арифметика) Теория чисел заменяет перебор всех значений обычной арифметикой. НОД, модульные правила и решето убирают полный перебор. ## Математика и теория чисел (НОД, решето, модульная арифметика): как это работает? Правило Евклида заменяет пару чисел меньшей парой. НОД чисел a и b равен НОД чисел b и a по модулю b. Цикл Евклида останавливается, когда второе число стало нулём. Оставшееся число и есть ответ. Наименьшее общее кратное следует из НОД. Это a умножить на b и поделить на их НОД. Решето вычёркивает кратные каждого встреченного простого. Всё невычеркнутое является простым. Модульная арифметика держит числа маленькими. Берите остаток после каждого шага, а не только в конце. У деления простой модульной формы нет. Вместо него нужен обратный элемент по модулю. Источники: [Википедия: Алгоритм Евклида](https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%95%D0%B2%D0%BA%D0%BB%D0%B8%D0%B4%D0%B0), [Википедия: Решето Эратосфена](https://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D1%88%D0%B5%D1%82%D0%BE_%D0%AD%D1%80%D0%B0%D1%82%D0%BE%D1%81%D1%84%D0%B5%D0%BD%D0%B0) - `нод(48, 18)` 48 по модулю 18 даёт 12. - `нод(18, 12)` 18 по модулю 12 даёт 6. - `нод(12, 6)` 12 по модулю 6 не даёт ничего. - `нод(6, 0) = 6` Второе число стало нулём. Ответ равен 6. - `нок = 48 на 18 делить на 6` Получается 144, и ни одно кратное не перечислялось. ## Математика и теория чисел (НОД, решето, модульная арифметика): когда применять? Паттерн «Математика и теория чисел (НОД, решето, модульная арифметика)» нужен, когда в условии есть слова «сократить дробь до несократимого вида», «перечислить все простые числа до n» или «ответ по модулю 1e9+7». - сократить дробь до несократимого вида - перечислить все простые числа до n - ответ по модулю 1e9+7 - когда снова совпадут два повторяющихся интервала (НОК) ## Математика и теория чисел (НОД, решето, модульная арифметика): с чем путают? Паттерн «Математика и теория чисел (НОД, решето, модульная арифметика)» чаще всего путают с этими паттернами: Битовые манипуляции, Хеш-множество / словарь, Динамическое программирование (1-D) и Префиксные суммы. - **Битовые манипуляции** - Оба работают с самим числом. Та страница про отдельные биты. - **Хеш-множество / словарь** - Множество простых чисел всё равно надо как-то построить. Решето и есть способ его построить. - **Динамическое программирование (1-D)** - Формула отвечает сразу там, где динамика заполняет таблицу. Сначала поищите замкнутую форму. - **Префиксные суммы** - Префиксные суммы отвечают по сохранённым значениям. Здесь ответ следует из свойств самих чисел. ## Математика и теория чисел (НОД, решето, модульная арифметика): сложность по времени и памяти НОД стоит O(log от меньшего значения). Решето до 1e7 нормально, а до 1e9 уже нет. ## Математика и теория чисел (НОД, решето, модульная арифметика): разбор примера ### Посчитать простые числа меньше n Посчитайте простые числа строго меньше n. Проверять каждое число по отдельности слишком медленно при большом n. Вычёркивайте все кратные каждого простого как составные. Начинайте вычёркивать с квадрата простого. У всего меньшего уже есть меньший делитель. ```javascript function countPrimes(n) { if (n < 3) return 0; const composite = new Array(n).fill(false); let count = 0; for (let p = 2; p < n; p++) { if (composite[p]) continue; count++; // start at p * p: everything smaller already has a smaller factor for (let multiple = p * p; multiple < n; multiple += p) { composite[multiple] = true; } } return count; } ``` ## Математика и теория чисел (НОД, решето, модульная арифметика): частые ошибки - **Берут остаток только в конце** Значение переполнится задолго до этого. Уменьшайте его после каждого умножения. - **Получают отрицательный остаток** В JavaScript минус один по модулю пять равен минус одному. Добавьте модуль и возьмите остаток снова. - **Начинают внутренний цикл решета с удвоенного простого** У всего ниже квадрата простого уже есть меньший делитель. Такой старт тратит большую часть работы. - **Проверяют делители дальше квадратного корня** Делитель выше корня всегда идёт в паре с делителем ниже. Останавливайтесь на корне. ## Математика и теория чисел (НОД, решето, модульная арифметика): задачи с собеседований Паттерн «Математика и теория чисел (НОД, решето, модульная арифметика)» решает такие задачи с собеседований: Наибольший общий делитель строк, Подсчёт простых чисел, Уродливые числа II, Степень тройки, Дробь в периодическую десятичную, Номер колонки в таблице и Счастливое число. - **Наибольший общий делитель строк** Длина ответа это НОД двух длин. - **Подсчёт простых чисел** Решето вместо проверки каждого числа. - **Уродливые числа II** Три указателя по кратным 2, 3 и 5. - **Степень тройки** Повторное деление или одна проверка делимости. - **Дробь в периодическую десятичную** Повторившийся остаток и отмечает период. - **Номер колонки в таблице** Система по основанию 26, но без нулевой цифры. - **Счастливое число** Поиск цикла по суммам квадратов цифр. ## JavaScript ```javascript function gcd(a, b) { while (b !== 0) { [a, b] = [b, a % b]; } return a; } function lcm(a, b) { return (a / gcd(a, b)) * b; } ``` ## TypeScript ```typescript function gcd(a: number, b: number): number { while (b !== 0) { [a, b] = [b, a % b]; } return a; } function lcm(a: number, b: number): number { return (a / gcd(a, b)) * b; } ``` ## Python ```python def gcd(a, b): while b != 0: a, b = b, a % b return a def lcm(a, b): return a // gcd(a, b) * b ``` ## PHP ```php function gcdEuclid(int $a, int $b): int { while ($b !== 0) { [$a, $b] = [$b, $a % $b]; } return $a; } function lcm(int $a, int $b): int { return intdiv($a, gcdEuclid($a, $b)) * $b; } ``` ## Java ```java public static int gcd(int a, int b) { while (b != 0) { int rest = a % b; a = b; b = rest; } return a; } public static long lcm(int a, int b) { return (long) (a / gcd(a, b)) * b; // divide first: the product can overflow } ``` ## C# ```csharp public static int Gcd(int a, int b) { while (b != 0) { int rest = a % b; a = b; b = rest; } return a; } public static long Lcm(int a, int b) { return (long) (a / Gcd(a, b)) * b; // divide first: the product can overflow } ``` --- title: "Обход матрицы (спираль / диагонали)" url: https://algopath.pro/ru/patterns/matrix-traversal language: ru summary: "Обходите сетку в том порядке, которого обычные циклы не дают. Следующую клетку выбирают границы или правило направления." updated: 2026-10-15 --- # Обход матрицы (спираль / диагонали) Обходите сетку в том порядке, которого обычные циклы не дают. Следующую клетку выбирают границы или правило направления. ## Обход матрицы (спираль / диагонали): как это работает? Спираль держит четыре границы: верх, низ, лево и право. Они сходятся внутрь по мере завершения колец. Пройдите верхний ряд слева направо, потом опустите верхнюю границу. Этот ряд уже использован. Пройдите правый столбец, потом нижний ряд, потом левый столбец. Каждый проход ужимает свою границу. Проверяйте границы перед двумя последними проходами. Иначе единственный оставшийся ряд пройдут дважды. Обход по диагоналям группирует клетки по сумме двух индексов. У всех клеток одной диагонали эта сумма одинакова. Направление переворачивается на каждой следующей диагонали. Именно это и даёт зигзаг. - `верхний ряд: 1, 2, 3` Сетка три на три, обход спиралью. - `правый столбец: 6, 9` Сверху вниз, потом правая граница уходит влево. - `нижний ряд: 8, 7` Справа налево, потом нижняя граница уходит вверх. - `левый столбец: 4` Снизу вверх, потом левая граница уходит вправо. - `центр: 5` Осталась одна клетка. Четыре границы встретились. ## Обход матрицы (спираль / диагонали): когда применять? Паттерн «Обход матрицы (спираль / диагонали)» нужен, когда в условии есть слова «прочитать или заполнить сетку по спирали», «обойти матрицу по диагоналям» или «повернуть матрицу на 90 градусов». Решение на нём обычно укладывается в O(mn). - прочитать или заполнить сетку по спирали - обойти матрицу по диагоналям - повернуть матрицу на 90 градусов - посетить каждую ячейку в фиксированном геометрическом порядке ## Обход матрицы (спираль / диагонали): с чем путают? Паттерн «Обход матрицы (спираль / диагонали)» чаще всего путают с этими паттернами: Поиск в матрице (отсортированная сетка), Поиск слова в матрице (DFS/backtracking по сетке), Обход графа BFS / DFS и Преобразование массива на месте. - **Поиск в матрице (отсортированная сетка)** - Там порядок используют, чтобы пропустить большую часть сетки. Здесь посещается каждая клетка. - **Поиск слова в матрице (DFS/backtracking по сетке)** - Там идёт перебор с откатом и разворотами. Здесь путь задан заранее. - **Обход графа BFS / DFS** - Обход идёт по рёбрам из стартовой вершины. Здесь порядок задаёт геометрия. - **Преобразование массива на месте** - Поворот матрицы на месте это та же идея в двух измерениях. Правило про один буфер то же самое. ## Обход матрицы (спираль / диагонали): сложность по времени и памяти У паттерна «Обход матрицы (спираль / диагонали)» целевая сложность O(mn). Сетка r на c даёт O(rc), потому что каждая клетка посещается один раз. Память O(1) сверх ответа. ## Обход матрицы (спираль / диагонали): разбор примера ### Повернуть квадратную матрицу на месте Поверните матрицу n на n на девяносто градусов по часовой стрелке, на месте. Выделять вторую матрицу нельзя. Сначала транспонируйте её, меняя каждую клетку с зеркальной через диагональ. Потом разверните каждый ряд. Эти два действия вместе и дают поворот. ```javascript function rotate(matrix) { const n = matrix.length; // transpose: j starts at i + 1, so no swap is undone for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]]; } } for (const row of matrix) row.reverse(); return matrix; } ``` ## Обход матрицы (спираль / диагонали): частые ошибки - **Транспонируют по всей сетке** Тогда каждый обмен отменяется своим же зеркальным. Начинайте внутренний цикл с i плюс один. - **Проходят оставшийся ряд дважды** Единственный средний ряд читается вперёд, а потом назад. Проверяйте границы перед двумя последними проходами. - **Считают сетку квадратной** Спираль по широкой сетке раньше исчерпывает столбцы. Границы это учитывают, фиксированное число кругов нет. - **Меняют местами индексы ряда и столбца** Обычный порядок это сначала ряд, потом столбец. Их перестановка молча транспонирует всё. ## Обход матрицы (спираль / диагонали): задачи с собеседований Паттерн «Обход матрицы (спираль / диагонали)» решает такие задачи с собеседований: Спиральный обход матрицы, Спиральное заполнение матрицы, Поворот изображения, Диагональный обход, Обнуление строк и столбцов, Транспонирование матрицы и Зигзагообразная запись. - **Спиральный обход матрицы** Четыре границы, сходящиеся внутрь кольцо за кольцом. - **Спиральное заполнение матрицы** Сетка в этом порядке заполняется, а не читается. - **Поворот изображения** Транспонировать, потом развернуть каждый ряд. - **Диагональный обход** Клетки группируются по сумме индексов. - **Обнуление строк и столбцов** Первый ряд и первый столбец служат пометками. - **Транспонирование матрицы** Кирпич, из которого собран поворот. - **Зигзагообразная запись** То же чередование направления, но по строке. ## JavaScript ```javascript function spiralOrder(matrix) { const result = []; let top = 0, bottom = matrix.length - 1; let left = 0, right = matrix[0].length - 1; while (top <= bottom && left <= right) { for (let c = left; c <= right; c++) result.push(matrix[top][c]); for (let r = top + 1; r <= bottom; r++) result.push(matrix[r][right]); if (top < bottom) for (let c = right - 1; c >= left; c--) result.push(matrix[bottom][c]); if (left < right) for (let r = bottom - 1; r > top; r--) result.push(matrix[r][left]); top++; bottom--; left++; right--; } return result; } ``` ## TypeScript ```typescript function spiralOrder(matrix: number[][]): number[] { const result: number[] = []; let top = 0, bottom = matrix.length - 1; let left = 0, right = matrix[0].length - 1; while (top <= bottom && left <= right) { for (let c = left; c <= right; c++) result.push(matrix[top][c]); for (let r = top + 1; r <= bottom; r++) result.push(matrix[r][right]); if (top < bottom) for (let c = right - 1; c >= left; c--) result.push(matrix[bottom][c]); if (left < right) for (let r = bottom - 1; r > top; r--) result.push(matrix[r][left]); top++; bottom--; left++; right--; } return result; } ``` ## Python ```python def spiral_order(matrix): result = [] top, bottom = 0, len(matrix) - 1 left, right = 0, len(matrix[0]) - 1 while top <= bottom and left <= right: result += [matrix[top][c] for c in range(left, right + 1)] result += [matrix[r][right] for r in range(top + 1, bottom + 1)] if top < bottom: result += [matrix[bottom][c] for c in range(right - 1, left - 1, -1)] if left < right: result += [matrix[r][left] for r in range(bottom - 1, top, -1)] top += 1; bottom -= 1; left += 1; right -= 1 return result ``` ## PHP ```php function spiralOrder(array $matrix): array { $result = []; $top = 0; $bottom = count($matrix) - 1; $left = 0; $right = count($matrix[0]) - 1; while ($top <= $bottom && $left <= $right) { for ($c = $left; $c <= $right; $c++) $result[] = $matrix[$top][$c]; for ($r = $top + 1; $r <= $bottom; $r++) $result[] = $matrix[$r][$right]; if ($top < $bottom) for ($c = $right - 1; $c >= $left; $c--) $result[] = $matrix[$bottom][$c]; if ($left < $right) for ($r = $bottom - 1; $r > $top; $r--) $result[] = $matrix[$r][$left]; $top++; $bottom--; $left++; $right--; } return $result; } ``` ## Java ```java public static List spiralOrder(int[][] matrix) { List result = new ArrayList<>(); int top = 0, bottom = matrix.length - 1; int left = 0, right = matrix[0].length - 1; while (top <= bottom && left <= right) { for (int c = left; c <= right; c++) result.add(matrix[top][c]); for (int r = top + 1; r <= bottom; r++) result.add(matrix[r][right]); if (top < bottom) for (int c = right - 1; c >= left; c--) result.add(matrix[bottom][c]); if (left < right) for (int r = bottom - 1; r > top; r--) result.add(matrix[r][left]); top++; bottom--; left++; right--; } return result; } ``` ## C# ```csharp public static List SpiralOrder(int[][] matrix) { var result = new List(); int top = 0, bottom = matrix.Length - 1; int left = 0, right = matrix[0].Length - 1; while (top <= bottom && left <= right) { for (int c = left; c <= right; c++) result.Add(matrix[top][c]); for (int r = top + 1; r <= bottom; r++) result.Add(matrix[r][right]); if (top < bottom) for (int c = right - 1; c >= left; c--) result.Add(matrix[bottom][c]); if (left < right) for (int r = bottom - 1; r > top; r--) result.Add(matrix[r][left]); top++; bottom--; left++; right--; } return result; } ``` --- title: "Поиск в матрице (отсортированная сетка)" url: https://algopath.pro/ru/patterns/matrix-search language: ru summary: "В сетке, отсортированной в обе стороны, из угла есть только одно направление. Сравнение отбрасывает целый ряд или столбец." updated: 2026-10-17 --- # Поиск в матрице (отсортированная сетка) В сетке, отсортированной в обе стороны, из угла есть только одно направление. Сравнение отбрасывает целый ряд или столбец. ## Поиск в матрице (отсортированная сетка): как это работает? Отсортированные сетки бывают двух видов. В одной каждый ряд продолжает предыдущий, в другой ряды и столбцы просто возрастают. Первый вид это отсортированный список, свёрнутый в ряды. Бинарный поиск идёт по r умножить на c позициям. Для второго вида начните с правого верхнего угла. Слева от него всё меньше, а снизу всё больше. Если значение там слишком велико, идите влево. Это сразу отбрасывает целый столбец. Если оно слишком мало, идите вниз. Это отбрасывает целый ряд. Каждый шаг убирает один ряд или один столбец. Поэтому обход кончается за r плюс c шагов. Источники: [Википедия: Двоичный поиск](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA) - `старт на 7, правый верх` Ищем 5 в сетке, возрастающей в обе стороны. - `7 слишком велика` Всё ниже семёрки в этом столбце больше. Идём влево. - `4 слишком мала` Всё левее четвёрки в этом ряду меньше. Идём вниз. - `теперь на 5` Значение под обходом совпало с искомым. - `три шага` Девять клеток, три сравнения. Две линии отброшены целиком. ## Поиск в матрице (отсортированная сетка): когда применять? Паттерн «Поиск в матрице (отсортированная сетка)» нужен, когда в условии есть слова «матрица отсортирована и по строкам, и по столбцам», «поиск значения в 2D отсортированной сетке» или «начать с угла и отбрасывать строку или столбец». Решение на нём обычно укладывается в O(m+n). - матрица отсортирована и по строкам, и по столбцам - поиск значения в 2D отсортированной сетке - начать с угла и отбрасывать строку или столбец - и строки, и столбцы монотонны ## Поиск в матрице (отсортированная сетка): с чем путают? Паттерн «Поиск в матрице (отсортированная сетка)» чаще всего путают с этими паттернами: Бинарный поиск (массив), Два указателя (с концов), Обход матрицы (спираль / диагонали) и Линейный поиск. - **Бинарный поиск (массив)** - Если каждый ряд продолжает предыдущий, сетка это один отсортированный список. Тогда работает обычный бинарный поиск. - **Два указателя (с концов)** - Ход лесенкой это та же идея в двух измерениях. За шаг двигается ровно один индекс. - **Обход матрицы (спираль / диагонали)** - Там посещается каждая клетка в выбранном порядке. Здесь большинство клеток не читается вовсе. - **Линейный поиск** - Перебор работает и стоит O(rc). Расточительным его делает как раз наличие порядка. ## Поиск в матрице (отсортированная сетка): сложность по времени и памяти У паттерна «Поиск в матрице (отсортированная сетка)» целевая сложность O(m+n). Сетка r на c даёт O(r + c) при ходе лесенкой. Полностью отсортированная сетка позволяет O(log rc). ## Поиск в матрице (отсортированная сетка): разбор примера ### K-е наименьшее значение в отсортированной сетке В сетке возрастают и ряды, и столбцы. Верните её k-е наименьшее значение. Развернуть и отсортировать можно, но это читает каждую клетку. Ведите бинарный поиск по диапазону значений, а не по позициям. Для кандидата посчитайте, сколько клеток не больше него. Это число и говорит, какую половину оставить. ```javascript function kthSmallest(matrix, k) { const n = matrix.length; let lo = matrix[0][0]; let hi = matrix[n - 1][n - 1]; const countNotAbove = (value) => { let count = 0; let row = n - 1; // one staircase walk, starting from the bottom-left corner for (let col = 0; col < n; col++) { while (row >= 0 && matrix[row][col] > value) row--; count += row + 1; } return count; }; while (lo < hi) { const mid = lo + Math.floor((hi - lo) / 2); if (countNotAbove(mid) >= k) hi = mid; else lo = mid + 1; } return lo; } ``` ## Поиск в матрице (отсортированная сетка): частые ошибки - **Стартуют не из того угла** Левая верхняя клетка меньше обоих соседей и направления не даёт. Начинайте справа сверху или слева снизу. - **Считают построчную сетку полностью отсортированной** Поиску по позициям нужно, чтобы каждый ряд продолжал предыдущий. Проверьте это до развёртки. - **Сортируют всю сетку** Так выбрасывается структура, которую вам дали. Плюс это стоит rc log rc. - **Возвращают среднего кандидата** При поиске по значению ответ обязан существовать в сетке. Сжимайте, пока границы не сойдутся. ## Поиск в матрице (отсортированная сетка): задачи с собеседований Паттерн «Поиск в матрице (отсортированная сетка)» решает такие задачи с собеседований: Поиск в двумерной матрице, Поиск в двумерной матрице II, K-е наименьшее в отсортированной матрице, Поиск пика в матрице, Число отрицательных в отсортированной матрице, Медиана построчно отсортированной матрицы и Наименьший общий элемент всех рядов. - **Поиск в двумерной матрице** Один отсортированный список, свёрнутый в ряды. - **Поиск в двумерной матрице II** Ход лесенкой из угла. - **K-е наименьшее в отсортированной матрице** Бинарный поиск по значениям с подсчётом на кандидата. - **Поиск пика в матрице** Бинарный поиск по столбцам. - **Число отрицательных в отсортированной матрице** Та же лесенка, но со счётом вместо совпадения. - **Медиана построчно отсортированной матрицы** Считайте, сколько клеток не больше кандидата. - **Наименьший общий элемент всех рядов** По указателю на ряд, двигаются вместе. ## JavaScript ```javascript function searchMatrix(matrix, target) { let row = 0; let col = matrix[0].length - 1; while (row < matrix.length && col >= 0) { const v = matrix[row][col]; if (v === target) return true; if (v > target) col--; else row++; } return false; } ``` ## TypeScript ```typescript function searchMatrix(matrix: number[][], target: number): boolean { let row = 0; let col = matrix[0].length - 1; while (row < matrix.length && col >= 0) { const v = matrix[row][col]; if (v === target) return true; if (v > target) col--; else row++; } return false; } ``` ## Python ```python def search_matrix(matrix, target): row, col = 0, len(matrix[0]) - 1 while row < len(matrix) and col >= 0: v = matrix[row][col] if v == target: return True if v > target: col -= 1 else: row += 1 return False ``` ## PHP ```php function searchMatrix(array $matrix, int $target): bool { $row = 0; $col = count($matrix[0]) - 1; while ($row < count($matrix) && $col >= 0) { $v = $matrix[$row][$col]; if ($v === $target) return true; if ($v > $target) $col--; else $row++; } return false; } ``` ## Java ```java public static boolean searchMatrix(int[][] matrix, int target) { int row = 0; int col = matrix[0].length - 1; while (row < matrix.length && col >= 0) { int v = matrix[row][col]; if (v == target) return true; if (v > target) col--; else row++; } return false; } ``` ## C# ```csharp public static bool SearchMatrix(int[][] matrix, int target) { int row = 0; int col = matrix[0].Length - 1; while (row < matrix.Length && col >= 0) { int v = matrix[row][col]; if (v == target) return true; if (v > target) col--; else row++; } return false; } ``` --- title: "Поиск слова в матрице (DFS/backtracking по сетке)" url: https://algopath.pro/ru/patterns/matrix-word-search language: ru summary: "Ищите в сетке, шагая в соседа и потом возвращаясь обратно. Клетка помечена ровно столько времени, сколько вы внутри неё." updated: 2026-10-17 --- # Поиск слова в матрице (DFS/backtracking по сетке) Ищите в сетке, шагая в соседа и потом возвращаясь обратно. Клетка помечена ровно столько времени, сколько вы внутри неё. ## Поиск слова в матрице (DFS/backtracking по сетке): как это работает? Слово может начинаться в любой клетке. Пробуйте поиск из каждой. В клетке сравните её с текущим символом. Несовпадение сразу обрывает эту ветку. Пометьте клетку использованной до шага дальше. Иначе путь пройдёт сам по себе. Попробуйте по очереди всех четырёх соседей. Слово может продолжиться через любого. Снимите пометку после возврата всех четырёх вызовов. Следующий поиск обязан увидеть клетку свободной. Дойти до конца слова значит найти его. Ниже этой точки ничего не важно. Источники: [Википедия: Поиск с возвратом](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%B8%D1%81%D0%BA_%D1%81_%D0%B2%D0%BE%D0%B7%D0%B2%D1%80%D0%B0%D1%82%D0%BE%D0%BC) - `старт в A, индекс 0` Сетка это A, B над C, D, а слово это ABD. - `сосед B, индекс 1` B совпадает со второй буквой, поэтому тоже помечается. - `из B: A помечена` Именно пометка не даёт пути повернуть назад. - `D, индекс 2` D совпадает с последней буквой. Слово собрано. - `ответ = истина` Помечены были две клетки, и обе очищаются на выходе. ## Поиск слова в матрице (DFS/backtracking по сетке): когда применять? Паттерн «Поиск слова в матрице (DFS/backtracking по сетке)» нужен, когда в условии есть слова «существует ли слово как путь по доске», «проследить буквы через соседние клетки без повторов» или «DFS с возвратом (backtracking) по сетке». Решение на нём обычно укладывается в O(mn*4^L). - существует ли слово как путь по доске - проследить буквы через соседние клетки без повторов - DFS с возвратом (backtracking) по сетке - пометить клетку посещённой, затем отменить пометку ## Поиск слова в матрице (DFS/backtracking по сетке): с чем путают? Паттерн «Поиск слова в матрице (DFS/backtracking по сетке)» чаще всего путают с этими паттернами: Бэктрекинг, Обход графа BFS / DFS, Компоненты связности и Trie (префиксное дерево). - **Бэктрекинг** - Это бэктрекинг, где деревом служит сетка. Вариантами служат четыре соседа. - **Обход графа BFS / DFS** - Обход помечает клетку раз и навсегда. Здесь пометка снимается на выходе. - **Компоненты связности** - Заливка тоже ходит по соседям, но пометки не снимает. Она считает, а не ищет. - **Trie (префиксное дерево)** - Когда слов много, trie обрезает обход. Без него каждое слово стоит своего поиска. ## Поиск слова в матрице (DFS/backtracking по сетке): сложность по времени и памяти У паттерна «Поиск слова в матрице (DFS/backtracking по сетке)» целевая сложность O(mn*4^L). Сетка r на c и слово длины L стоят O(rc умножить на 4^L). Работает это за счёт обрезки. ## Поиск слова в матрице (DFS/backtracking по сетке): разбор примера ### Найти все слова, спрятанные в сетке Даны сетка букв и список слов. Верните все слова, которые есть в сетке. Поиск каждого слова по отдельности повторяет огромный объём работы. Сначала сложите все слова в trie. Потом пройдите сетку один раз, неся узел trie вместо индекса в слове. Отсутствующий потомок обрезает ветку. ```javascript function findWords(board, words) { const root = { children: {} }; for (const word of words) { let node = root; for (const c of word) { node.children[c] = node.children[c] ?? { children: {} }; node = node.children[c]; } node.word = word; } const found = []; function walk(r, c, node) { const letter = board[r]?.[c]; const next = letter && node.children[letter]; if (!next) return; // no word continues this way if (next.word) { found.push(next.word); next.word = null; // never report the same word twice } board[r][c] = "#"; // marked only while we are inside this cell walk(r + 1, c, next); walk(r - 1, c, next); walk(r, c + 1, next); walk(r, c - 1, next); board[r][c] = letter; } for (let r = 0; r < board.length; r++) { for (let c = 0; c < board[0].length; c++) walk(r, c, root); } return found; } ``` ## Поиск слова в матрице (DFS/backtracking по сетке): частые ошибки - **Забывают снять пометку** Клетка остаётся закрытой для всех последующих поисков. Восстанавливайте её после четырёх вызовов. - **Помечают настоящей буквой** Метка, которая встречается в словах, позволяет пути пройти клетку заново. Возьмите символ вне алфавита. - **Проверяют границы после чтения** Чтение вне сетки падает или даёт undefined. Проверяйте координаты до обращения. - **Ищут каждое слово отдельно** Сто слов означают сто полных обходов. Один trie сводит их к единственному обходу. ## Поиск слова в матрице (DFS/backtracking по сетке): задачи с собеседований Паттерн «Поиск слова в матрице (DFS/backtracking по сетке)» решает такие задачи с собеседований: Поиск слова в сетке, Поиск слов в сетке II, Число островов, Путь с наибольшим золотом, Уникальные пути III, Крыса в лабиринте и Решатель судоку. - **Поиск слова в сетке** Одно слово, четыре соседа на каждом шаге. - **Поиск слов в сетке II** Trie обрезает обход, когда слов много. - **Число островов** Тот же обход, только пометки не снимаются. - **Путь с наибольшим золотом** Соберите значение и верните его на выходе. - **Уникальные пути III** Каждую свободную клетку надо посетить ровно один раз. - **Крыса в лабиринте** Та же форма, но записывается сам путь. - **Решатель судоку** Сетка, где вариантами служат цифры, а не направления. ## JavaScript ```javascript function exist(board, word) { const rows = board.length, cols = board[0].length; function dfs(r, c, i) { if (i === word.length) return true; if (r < 0 || r >= rows || c < 0 || c >= cols || board[r][c] !== word[i]) return false; const tmp = board[r][c]; board[r][c] = "#"; const found = dfs(r + 1, c, i + 1) || dfs(r - 1, c, i + 1) || dfs(r, c + 1, i + 1) || dfs(r, c - 1, i + 1); board[r][c] = tmp; return found; } for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (dfs(r, c, 0)) return true; } } return false; } ``` ## TypeScript ```typescript function exist(board: string[][], word: string): boolean { const rows = board.length, cols = board[0].length; function dfs(r: number, c: number, i: number): boolean { if (i === word.length) return true; if (r < 0 || r >= rows || c < 0 || c >= cols || board[r][c] !== word[i]) return false; const tmp = board[r][c]; board[r][c] = "#"; const found = dfs(r + 1, c, i + 1) || dfs(r - 1, c, i + 1) || dfs(r, c + 1, i + 1) || dfs(r, c - 1, i + 1); board[r][c] = tmp; return found; } for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (dfs(r, c, 0)) return true; } } return false; } ``` ## Python ```python def exist(board, word): rows, cols = len(board), len(board[0]) def dfs(r, c, i): if i == len(word): return True if r < 0 or r >= rows or c < 0 or c >= cols or board[r][c] != word[i]: return False tmp, board[r][c] = board[r][c], "#" found = (dfs(r + 1, c, i + 1) or dfs(r - 1, c, i + 1) or dfs(r, c + 1, i + 1) or dfs(r, c - 1, i + 1)) board[r][c] = tmp return found return any(dfs(r, c, 0) for r in range(rows) for c in range(cols)) ``` ## PHP ```php function exist(array &$board, string $word): bool { $rows = count($board); $cols = count($board[0]); $dfs = function (int $r, int $c, int $i) use (&$board, &$dfs, $word, $rows, $cols): bool { if ($i === strlen($word)) return true; if ($r < 0 || $r >= $rows || $c < 0 || $c >= $cols || $board[$r][$c] !== $word[$i]) return false; $tmp = $board[$r][$c]; $board[$r][$c] = "#"; $found = $dfs($r + 1, $c, $i + 1) || $dfs($r - 1, $c, $i + 1) || $dfs($r, $c + 1, $i + 1) || $dfs($r, $c - 1, $i + 1); $board[$r][$c] = $tmp; return $found; }; for ($r = 0; $r < $rows; $r++) { for ($c = 0; $c < $cols; $c++) { if ($dfs($r, $c, 0)) return true; } } return false; } ``` ## Java ```java public static boolean exist(char[][] board, String word) { for (int r = 0; r < board.length; r++) { for (int c = 0; c < board[0].length; c++) { if (dfs(board, word, r, c, 0)) return true; } } return false; } private static boolean dfs(char[][] board, String word, int r, int c, int i) { if (i == word.length()) return true; if (r < 0 || r >= board.length || c < 0 || c >= board[0].length || board[r][c] != word.charAt(i)) return false; char tmp = board[r][c]; board[r][c] = '#'; boolean found = dfs(board, word, r + 1, c, i + 1) || dfs(board, word, r - 1, c, i + 1) || dfs(board, word, r, c + 1, i + 1) || dfs(board, word, r, c - 1, i + 1); board[r][c] = tmp; return found; } ``` ## C# ```csharp public static bool Exist(char[][] board, string word) { for (int r = 0; r < board.Length; r++) { for (int c = 0; c < board[0].Length; c++) { if (Dfs(board, word, r, c, 0)) return true; } } return false; } private static bool Dfs(char[][] board, string word, int r, int c, int i) { if (i == word.Length) return true; if (r < 0 || r >= board.Length || c < 0 || c >= board[0].Length || board[r][c] != word[i]) return false; char tmp = board[r][c]; board[r][c] = '#'; bool found = Dfs(board, word, r + 1, c, i + 1) || Dfs(board, word, r - 1, c, i + 1) || Dfs(board, word, r, c + 1, i + 1) || Dfs(board, word, r, c - 1, i + 1); board[r][c] = tmp; return found; } ``` --- title: "Дерево Фенвика / дерево отрезков" url: https://algopath.pro/ru/patterns/fenwick-segment-tree language: ru summary: "Дерево над отрезками отвечает на запрос и принимает изменение, и то и другое за log n. Префиксные суммы второго не умеют." updated: 2026-10-17 --- # Дерево Фенвика / дерево отрезков Дерево над отрезками отвечает на запрос и принимает изменение, и то и другое за log n. Префиксные суммы второго не умеют. ## Дерево Фенвика / дерево отрезков: как это работает? Дерево Фенвика хранит частичные суммы в массиве с индексами от 1. tree[i] покрывает блок, который кончается на i. Длина блока равна младшему единичному биту i, это i & -i. Поэтому tree[4] покрывает позиции с 1 по 4, а tree[6] позиции 5 и 6. query(i) прибавляет tree[i] и снимает с i младший бит. Цикл идёт, пока i не станет нулём, и возвращает сумму префикса. update(i) прибавляет изменение к tree[i] и добавляет к i младший бит. Так изменение получает каждый блок, где лежит позиция i. Оба цикла трогают около log n ячеек. Сумма отрезка это query(r) минус query(l - 1). Дерево отрезков держит узел на каждый отрезок, и его сумма складывается из двух потомков. Запрос останавливается на узлах, целиком лежащих внутри отрезка. В дереве отрезков больше кода, чем в дереве Фенвика. Зато оно умеет минимум, максимум и отложенные изменения на отрезке. Источники: [Википедия: Дерево Фенвика](https://ru.wikipedia.org/wiki/%D0%94%D0%B5%D1%80%D0%B5%D0%B2%D0%BE_%D0%A4%D0%B5%D0%BD%D0%B2%D0%B8%D0%BA%D0%B0), [Википедия: Дерево отрезков](https://ru.wikipedia.org/wiki/%D0%94%D0%B5%D1%80%D0%B5%D0%B2%D0%BE_%D0%BE%D1%82%D1%80%D0%B5%D0%B7%D0%BA%D0%BE%D0%B2), [Fenwick (1994), A new data structure for cumulative frequency tables (англ.)](https://doi.org/10.1002/spe.4380240306) - `tree = [3, 4, 4, 9]` Массив это [3, 1, 4, 1], индексы с 1. tree[4] покрывает все четыре. - `query(3): tree[3] + tree[2]` 3 теряет младший бит и становится 2. Пока набрано 4 плюс 4. - `query(3) = 8` 2 теряет младший бит и становится 0. Сумма 8 это 3 + 1 + 4. - `update(2, +5): tree[2] и tree[4]` 2 плюс младший бит даёт 4, а 8 уже за концом. Меняются две ячейки. - `сумма с 2 по 3 = 13 - 3 = 10` query(3) теперь 13, а query(1) равно 3. Разность и есть отрезок. ## Дерево Фенвика / дерево отрезков: когда применять? Паттерн «Дерево Фенвика / дерево отрезков» нужен, когда в условии есть слова «сумма или запрос на отрезке с точечными обновлениями вперемешку», «живой лидерборд или ранг, пока баллы меняются» или «подсчёт инверсий или сколько более ранних значений больше». Решение на нём обычно укладывается в O(log n) per op. - сумма или запрос на отрезке с точечными обновлениями вперемешку - живой лидерборд или ранг, пока баллы меняются - подсчёт инверсий или сколько более ранних значений больше - запросы на отрезке, пока данные продолжают меняться ## Дерево Фенвика / дерево отрезков: с чем путают? Паттерн «Дерево Фенвика / дерево отрезков» чаще всего путают с этими паттернами: Префиксные суммы, Массив разностей, Линия развёртки (подсчёт событий) и Бинарный поиск (массив). - **Префиксные суммы** - Префиксные суммы читаются быстрее, но ломаются при любом изменении. Здесь платят log n за право менять. - **Массив разностей** - Он принимает много изменений на отрезках и одно чтение в конце. Здесь чтения и записи перемешаны. - **Линия развёртки (подсчёт событий)** - Развёртка идёт по событиям в порядке сортировки и не оглядывается. Здесь запросы приходят в любом порядке. - **Бинарный поиск (массив)** - Оба делят отрезок пополам на каждом шаге. Один ищет значение, другой сворачивает отрезок. ## Дерево Фенвика / дерево отрезков: сложность по времени и памяти У паттерна «Дерево Фенвика / дерево отрезков» целевая сложность O(log n) per op. n до 1e6 и q смешанных операций дают O((n + q) log n). Память O(n) для дерева Фенвика. ## Дерево Фенвика / дерево отрезков: разбор примера ### Сколько меньших значений стоит справа Для каждого элемента посчитайте, сколько более поздних элементов меньше него. Вложенный цикл это O(n в квадрате), и он умирает на 1e5 элементов. Идите по массиву справа налево, держа дерево Фенвика над значениями. Для каждого элемента спросите, сколько меньших уже записано. Потом запишите текущий. ```javascript function countSmaller(nums) { const sorted = [...new Set(nums)].sort((a, b) => a - b); const rank = new Map(sorted.map((v, i) => [v, i + 1])); // ranks start at 1 const tree = new Array(sorted.length + 1).fill(0); const add = (i) => { for (; i < tree.length; i += i & -i) tree[i]++; }; const countBelow = (i) => { let total = 0; for (; i > 0; i -= i & -i) total += tree[i]; return total; }; const answer = new Array(nums.length); for (let i = nums.length - 1; i >= 0; i--) { const r = rank.get(nums[i]); answer[i] = countBelow(r - 1); // strictly smaller values already seen add(r); } return answer; } ``` ## Дерево Фенвика / дерево отрезков: частые ошибки - **Индексируют дерево Фенвика с нуля** Шагу по младшему биту нужны индексы с единицы. Нулевой индекс зацикливает цикл. - **Берут его там, где ничего не меняется** Префиксный массив отвечает за O(1) и ничего не стоит при чтении. Платите за дерево, только когда есть изменения. - **Забывают сжать значения** Дерево размером с диапазон значений умирает на числах до 1e9. Сначала переведите их в ранги. - **Сливают отрезки неподходящим правилом** Сумма и минимум работают, потому что порядок им не важен. Правилу, зависящему от порядка, нужно больше в узле. ## Дерево Фенвика / дерево отрезков: задачи с собеседований Паттерн «Дерево Фенвика / дерево отрезков» решает такие задачи с собеседований: Сумма на отрезке с изменениями, Сколько меньших справа, Число сумм в диапазоне, Обратные пары, Минимум на отрезке, Сумма на прямоугольнике с изменениями и Мой календарь II. - **Сумма на отрезке с изменениями** Чтения и записи идут вперемешку в любом порядке. - **Сколько меньших справа** Дерево Фенвика над сжатыми рангами. - **Число сумм в диапазоне** Префиксные суммы, поданные в дерево. - **Обратные пары** Подойдёт и сортировка слиянием, и дерево Фенвика. - **Минимум на отрезке** Дерево отрезков, поскольку у минимума нет обратной операции. - **Сумма на прямоугольнике с изменениями** Дерево, узлами которого служат другие деревья. - **Мой календарь II** Дерево отрезков с отложенными изменениями на отрезке. ## JavaScript ```javascript class Fenwick { constructor(n) { this.tree = new Array(n + 1).fill(0); } update(i, delta) { for (; i < this.tree.length; i += i & -i) this.tree[i] += delta; } query(i) { let sum = 0; for (; i > 0; i -= i & -i) sum += this.tree[i]; return sum; } } ``` ## TypeScript ```typescript class Fenwick { private tree: number[]; constructor(n: number) { this.tree = new Array(n + 1).fill(0); } update(i: number, delta: number): void { for (; i < this.tree.length; i += i & -i) this.tree[i] += delta; } query(i: number): number { let sum = 0; for (; i > 0; i -= i & -i) sum += this.tree[i]; return sum; } } ``` ## Python ```python class Fenwick: def __init__(self, n): self.tree = [0] * (n + 1) def update(self, i, delta): while i < len(self.tree): self.tree[i] += delta i += i & -i def query(self, i): total = 0 while i > 0: total += self.tree[i] i -= i & -i return total ``` ## PHP ```php class Fenwick { private array $tree; public function __construct(int $n) { $this->tree = array_fill(0, $n + 1, 0); } public function update(int $i, int $delta): void { for (; $i < count($this->tree); $i += $i & -$i) $this->tree[$i] += $delta; } public function query(int $i): int { $sum = 0; for (; $i > 0; $i -= $i & -$i) $sum += $this->tree[$i]; return $sum; } } ``` ## Java ```java public static class Fenwick { private final int[] tree; public Fenwick(int n) { tree = new int[n + 1]; } public void update(int i, int delta) { for (; i < tree.length; i += i & -i) tree[i] += delta; } public int query(int i) { int sum = 0; for (; i > 0; i -= i & -i) sum += tree[i]; return sum; } } ``` ## C# ```csharp public class Fenwick { private readonly int[] tree; public Fenwick(int n) { tree = new int[n + 1]; } public void Update(int i, int delta) { for (; i < tree.Length; i += i & -i) tree[i] += delta; } public int Query(int i) { int sum = 0; for (; i > 0; i -= i & -i) sum += tree[i]; return sum; } } ``` ## Бесплатные уроки - [Что такое алгоритм?](https://algopath.pro/ru/steps/what-is-an-algorithm) - [Инструкции, которые компьютер выполняет буквально](https://algopath.pro/ru/steps/instructions-a-computer-follows) - [Данные и переменные: коробка с наклейкой](https://algopath.pro/ru/steps/data-and-variables) - [Список: ряд пронумерованных коробок](https://algopath.pro/ru/steps/a-list-of-boxes) - [Сделать для каждого: циклы](https://algopath.pro/ru/steps/loops-do-it-for-each) - [Делаем выбор: если / иначе](https://algopath.pro/ru/steps/making-choices-if-else) - [Твоя первая программа: сложить два числа](https://algopath.pro/ru/steps/first-program-add-two-numbers) - [Найти самое большое число](https://algopath.pro/ru/steps/find-the-biggest-number) - [Почему одни способы быстрее](https://algopath.pro/ru/steps/why-some-ways-are-faster) - [Big-O: имя для того, как быстро растёт работа](https://algopath.pro/ru/steps/big-o-and-constraints) - [Перестроить массив, не заводя второй](https://algopath.pro/ru/steps/arrays-strings-in-place) - [Множество: мешок, который не терпит повторов](https://algopath.pro/ru/steps/collections-sets) - [Map: помеченное хранилище ключ → значение](https://algopath.pro/ru/steps/collections-maps) - [WeakMap и WeakSet: привязываем данные к объектам](https://algopath.pro/ru/steps/collections-weak) - [Как хеш-таблица находит ячейку](https://algopath.pro/ru/steps/hashing-buckets) - [Коллизии, решённые цепочками](https://algopath.pro/ru/steps/hashing-chaining) - [Коллизии через пробирование (открытая адресация)](https://algopath.pro/ru/steps/hashing-open-addressing) - [Заполненность и расширение таблицы](https://algopath.pro/ru/steps/hashing-resize) - [«Я это уже видел?» за один шаг](https://algopath.pro/ru/steps/hashing-map-set-frequency) - [Сходимся с двух концов](https://algopath.pro/ru/steps/two-pointers-opposite-ends) - [Убираем повторы прямо в массиве](https://algopath.pro/ru/steps/two-pointers-same-direction) - [Переиспользуем работу прошлого окна](https://algopath.pro/ru/steps/sliding-window-fixed) - [Окно, которое меняет размер под правило](https://algopath.pro/ru/steps/sliding-window-variable) - [Окно, которое помнит своё содержимое](https://algopath.pro/ru/steps/sliding-window-hash-counter) - [Сумма любого куска списка за один шаг](https://algopath.pro/ru/steps/prefix-sums) - [Считаем отрезки, которые дают в сумме k](https://algopath.pro/ru/steps/subarray-sum-k) - [Суммируем любой прямоугольник за один шаг](https://algopath.pro/ru/steps/prefix-2d) - [Поднять весь диапазон, не трогая каждую ячейку](https://algopath.pro/ru/steps/difference-arrays) - [Поиск элемента простым перебором](https://algopath.pro/ru/steps/linear-search) - [Поиск элемента в отсортированном списке делением пополам](https://algopath.pro/ru/steps/binary-search-array) - [Найти точное место, где «нет» превращается в «да»](https://algopath.pro/ru/steps/binary-search-boundary) - [Бинарный поиск в повёрнутом отсортированном массиве](https://algopath.pro/ru/steps/search-rotated) - [Угадать ответ и бинарно искать саму догадку](https://algopath.pro/ru/steps/binary-search-on-answer) ## Статьи - [seo.sections.blog.index · Как готовиться](https://algopath.pro/ru/blog/category/how-to-prepare) - [seo.sections.blog.index · Алгоритмы по шагам](https://algopath.pro/ru/blog/category/algorithms) - [seo.sections.blog.index · Обновления](https://algopath.pro/ru/blog/category/product-updates) --- title: "Слияние интервалов: сортировка, один проход и четыре задачи вокруг" url: https://algopath.pro/ru/blog/sliyanie-intervalov language: ru summary: "Сортируем по началу и склеиваем с последним в ответе. Сигналы в условии, код на Python для Merge и Insert Interval, разбор Meeting Rooms II и Non-overlapping Intervals, ошибки." updated: 2026-10-27 --- # Слияние интервалов: сортировка, один проход и четыре задачи вокруг **Короткий ответ.** Слияние интервалов делается в два шага. Сортируем отрезки по началу, потом идём по ним и склеиваем каждый с последним в ответе, если они пересекаются. Время O(n log n) из-за сортировки. Приём нужен в задачах про встречи, занятые слоты, вставку отрезка и покрытие. Ниже сигналы, код на Python, разбор и типичные ошибки. ## Что такое слияние интервалов? Интервал это пара чисел: начало и конец. Два интервала пересекаются, если начало второго не больше конца первого. Слияние заменяет пару пересекающихся отрезков одним, от меньшего начала до большего конца. Без сортировки пришлось бы сравнивать каждый отрезок с каждым. Это O(n²). После сортировки по началу пересечься с текущим может только последний склеенный. Отсюда весь приём: отсортировать, потом один проход. Слово «интервал» в задачах значит разное: время встречи, отрезок на прямой, диапазон индексов. Приём одинаков для всех трёх. Важно только, что есть начало и конец, и их можно сравнивать. ## По каким словам в задаче узнать слияние интервалов? - «Объединить пересекающиеся отрезки». - «Вставить новый интервал в список без пересечений». - «Сколько переговорок нужно для встреч». - «Может ли один человек посетить все встречи». Общий признак: дан список пар начало-конец, и вопрос про пересечения или покрытие. Если вопрос «сколько отрезков одновременно», это соседний приём, линия развёртки. ## Как работает слияние интервалов по шагам? - Сортируем отрезки по началу. - Кладём первый в ответ. - Для каждого следующего смотрим на последний в ответе. Начало текущего не больше его конца, значит пересекаются. Обновляем конец последнего максимумом двух концов. - Иначе кладём текущий в ответ как новый отрезок. Максимум концов нужен потому, что текущий отрезок может целиком лежать внутри предыдущего. ## Как это выглядит на примере? Отрезки [1, 3], [8, 10], [2, 6], [15, 18]. После сортировки: [1, 3], [2, 6], [8, 10], [15, 18]. Ответ: [1, 3]. Берём [2, 6]: 2 не больше 3, склеиваем, ответ [1, 6]. Берём [8, 10]: 8 больше 6, новый отрезок. Берём [15, 18]: 15 больше 10, новый. Итог [1, 6], [8, 10], [15, 18]. ## Как написать слияние интервалов на Python? ``` def merge(intervals): intervals.sort(key=lambda x: x[0]) result = [] for start, end in intervals: if result and start <= result[-1][1]: result[-1][1] = max(result[-1][1], end) # пересекаются else: result.append([start, end]) return result def insert(intervals, new): result, i, n = [], 0, len(intervals) while i < n and intervals[i][1] < new[0]: # целиком левее result.append(intervals[i]); i += 1 while i < n and intervals[i][0] <= new[1]: # пересекаются с новым new = [min(new[0], intervals[i][0]), max(new[1], intervals[i][1])] i += 1 result.append(new) result.extend(intervals[i:]) # целиком правее return result ``` Вторая функция вставляет отрезок в уже отсортированный список без сортировки. Три цикла: до, пересечение, после. Время O(n). ## Чем слияние отличается от соседних задач на интервалы? | Задача | Сортировка по | Что делаем в проходе | Время | | --- | --- | --- | --- | | Merge Intervals | Началу | Склеиваем с последним | O(n log n) | | Insert Interval | Уже отсортировано | Три цикла: до, внутри, после | O(n) | | Meeting Rooms | Началу | Ищем начало раньше прошлого конца | O(n log n) | | Meeting Rooms II | Началу плюс куча концов | Считаем максимум одновременных | O(n log n) | | Non-overlapping Intervals | Концу | Жадно оставляем самый ранний конец | O(n log n) | ## Как решить Meeting Rooms II? Дан список встреч. Нужно минимальное число переговорок. Сортируем по началу. Держим кучу концов идущих встреч. Для каждой встречи: если самый ранний конец в куче не больше её начала, та встреча закончилась, снимаем конец. Кладём конец текущей. Размер кучи в пике и есть ответ. ## Как решить Non-overlapping Intervals? Нужно удалить минимум отрезков, чтобы остальные не пересекались. Сортируем по концу. Держим конец последнего оставленного. Отрезок начинается раньше этого конца, удаляем его. Иначе оставляем и обновляем конец. Сортировка по концу тут обязательна, по началу жадность ломается. ## Как решить Meeting Rooms? Может ли один человек посетить все встречи? Сортируем по началу. Идём по парам соседей. Если начало следующей встречи меньше конца предыдущей, две встречи пересекаются, ответ нет. Дошли до конца без пересечений, ответ да. Это слияние без самого слияния. ## На каких ошибках ломается слияние интервалов? - Забывают сортировку и склеивают только соседей по входу. - Берут конец текущего вместо максимума концов. Вложенный отрезок укорачивает ответ. - Путают строгое и нестрогое сравнение на касании. [1, 3] и [3, 5] по условию могут быть и одним, и двумя. - Меняют кортежи вместо списков и получают ошибку присваивания. ## Как объяснить приём на собеседовании? Скажите: сортирую по началу, тогда пересечься с текущим может только последний склеенный. Назовите O(n log n) и объясните, что это сортировка, а проход линейный. Уточните у собеседующего, считается ли касание пересечением. Это показывает внимание к краям. Отработать узнавание приёма можно на странице [интервалы: слияние и вставка](/ru/patterns/intervals-merge). Соседний приём разобран на странице [линия развёртки](/ru/patterns/sweep-line). ## Вопросы и ответы ### Можно ли обойтись без сортировки? Только если вход уже отсортирован или координаты маленькие целые. Во втором случае подходит массив разностей за O(n + max). ### Как найти пересечение двух списков интервалов? Два указателя, по одному на список. Пересечение это максимум начал и минимум концов, если первое не больше второго. Двигаем тот указатель, чей отрезок кончается раньше. ### Что делать с открытыми и закрытыми концами? Решить один раз в начале и держать одно сравнение. Для закрытых концов пересечение при start не больше end. Для открытых при строгом меньше. ### Где приём встречается в реальных системах? В календарях при поиске свободного слота. В планировщиках, которые сливают занятые окна. В работе с диском, где склеивают соседние свободные блоки. --- title: "Как работает хеш-таблица" url: https://algopath.pro/ru/blog/kak-rabotaet-hesh-tablitsa language: ru summary: "Хеш-таблица превращает ключ в индекс массива, и этот шаг объясняет все правила, которые тебе велели соблюдать. Константное время это среднее." updated: 2026-10-27 --- # Как работает хеш-таблица **Короткий ответ.** Хеш-таблица прогоняет ключ через хеш-функцию, берёт остаток от деления на размер массива и кладёт значение в эту ячейку. Чтение повторяет те же шаги, поэтому в среднем занимает константное время. Коллизии решают цепочками или открытой адресацией, а после заполнения примерно на 0,75 таблица удваивает массив. ## Главный ход Есть массив, скажем на 16 ячеек. Прогоняем ключ через хеш-функцию. Получаем число. Берём остаток от деления на 16. Получилась ячейка. Кладём значение в неё. Чтение повторяет те же шаги и попадает туда же. Поиска не было. Отсюда и константное время. У [хеш-функции](https://ru.wikipedia.org/wiki/%D0%A5%D0%B5%D1%88-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F) одна работа. Равномерно раскидывать ключи по ячейкам. Хеш-функция всегда даёт один ответ для одного ключа. ## Коллизии Два ключа обязательно попадут в одну ячейку. При 16 ячейках и 20 ключах это гарантировано. Вероятно гораздо раньше. Стандартных ответов два. **Цепочки.** В ячейке лежит маленький список. Столкнулись, дописали в конец. Чтение находит ячейку и проходит список. Обычно там один или два элемента. **Открытая адресация.** Ячейка занята? Пробуем следующую. И так до свободной. Списков нет. С кешем дружелюбнее. Удаление капризнее. Оба быстрые, пока списки короткие. ## Заполнение и расширение За короткие списки отвечает коэффициент заполнения. Это записи, делённые на ячейки. После примерно 0,75 коллизии становятся частыми, чтение замедляется. Таблица берёт массив побольше, обычно вдвое. И перехеширует туда все ключи. Расширение стоит O(n). Платит за него одна невезучая вставка. В среднем по всем вставкам цена всё равно константная. Это честный смысл [амортизированной](https://ru.wikipedia.org/wiki/%D0%90%D0%BC%D0%BE%D1%80%D1%82%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D1%8B%D0%B9_%D0%B0%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7) оценки. Что значит константное время, разобрано в [Big O без математики](/ru/blog/big-o-prostymi-slovami). Отсюда же сюрприз в проде. Вставка, которая обычно занимает микросекунду, иногда занимает сильно больше. Если ты в этот момент держишь блокировку, это важно. ## Почему худший случай O(n) Все ключи в одной ячейке, и таблица вырождается в длинный список. Каждое чтение его сканирует. На случайных данных так не бывает. На подобранных бывает. Подобранные коллизии использовали как атаку. Отправить серверу тысячи намеренно сталкивающихся ключей и смотреть, как горит процессор. Поэтому современные среды солят хеш-функцию случайным числом на каждый процесс. На собеседовании отвечай «O(1) в среднем, O(n) в худшем». Проверяют как раз знание, что это среднее. ## Два правила, которые теперь понятны **Не меняй ключ после вставки.** Ячейку выбрали по старому содержимому. Изменил, и запись лежит не там. Она недостижима, а не потеряна. Это хуже. **Равные ключи обязаны хешироваться одинаково.** Переопределил равенство и не тронул хеш? Словарь начнёт терять записи. Баг всплывает через месяцы. ## Когда нужно другое У хеш-таблицы нет порядка. Это сделка, а не упущение. Поэтому «все ключи от 10 до 50» это полный проход. И «самый маленький ключ». И «всё по порядку». Таким запросам нужно сбалансированное дерево. Log n на операцию, порядок по построению. Базы данных держат обе. Хеш-индексы для точных совпадений, [B-деревья](https://ru.wikipedia.org/wiki/B-%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE) для диапазонов. ## Где это в треке [AlgoPath](/ru) тратит шаги с 14 по 17 на то, чтобы ты её построил. Ты сам напишешь код для коллизий и расширения, а не прочитаешь абзац. Когда за ней вообще тянуться, разобрано в статье [множество, словарь или массив](/ru/blog/struktury-dannyh-javascript). Паттерн, который на ней стоит, это [хеш-множество и словарь](/ru/patterns/hash-set-map). Первые 33 шага бесплатные. --- title: "Бинарный поиск в повёрнутом массиве: одна половина всегда отсортирована" url: https://algopath.pro/ru/blog/binarnyy-poisk-v-povyornutom-massive language: ru summary: "Как за O(log n) искать в массиве, который сдвинули по кругу. Идея, код на Python для поиска цели и минимума, разбор на примере, случай с дубликатами и типичные ошибки." updated: 2026-10-24 --- # Бинарный поиск в повёрнутом массиве: одна половина всегда отсортирована **Короткий ответ.** Бинарный поиск в повёрнутом массиве работает. Причина: одна из двух половин всегда отсортирована. На каждом шаге находим упорядоченную половину. Потом проверяем, лежит ли цель в ней. Время O(log n). Приём нужен для задач Search in Rotated Sorted Array и Find Minimum. Ниже идея, код на Python, разбор и ошибки. ## Что такое повёрнутый массив? Берём отсортированный массив [1, 2, 3, 4, 5, 6, 7]. Сдвигаем его по кругу на три позиции. Получаем [4, 5, 6, 7, 1, 2, 3]. Это и есть повёрнутый массив. Он состоит из двух отсортированных кусков. Точка, где большой элемент сменяется маленьким, называется точкой поворота. Здесь это переход от 7 к 1. Обычный бинарный поиск на таком массиве ломается. Середина не говорит, в какую сторону идти. Нужна одна дополнительная проверка, и она разобрана ниже. ## Как бинарный поиск в повёрнутом массиве выбирает половину? Берём середину mid. Сравниваем nums[lo] и nums[mid]. Если nums[lo] не больше nums[mid], левая половина отсортирована. Иначе отсортирована правая. Дальше смотрим на цель. Если цель лежит внутри отсортированной половины по значениям, идём туда. Иначе идём в другую половину. На каждом шаге диапазон уменьшается вдвое. Отсюда O(log n). ## Как это выглядит на примере? Массив [4, 5, 6, 7, 0, 1, 2], цель 0. lo = 0, hi = 6, mid = 3, nums[mid] = 7. nums[lo] = 4 не больше 7, левая половина отсортирована: от 4 до 7. Цель 0 не в ней, идём вправо: lo = 4. mid = 5, nums[mid] = 1. nums[lo] = 0 не больше 1, левая половина [0, 1] отсортирована. Цель 0 в ней, hi = 4. mid = 4, nums[mid] = 0. Нашли, ответ 4. ## Как написать бинарный поиск в повёрнутом массиве на Python? ``` def search(nums, target): lo, hi = 0, len(nums) - 1 while lo <= hi: mid = (lo + hi) // 2 if nums[mid] == target: return mid if nums[lo] <= nums[mid]: # левая половина отсортирована if nums[lo] <= target < nums[mid]: hi = mid - 1 else: lo = mid + 1 else: # правая половина отсортирована if nums[mid] < target <= nums[hi]: lo = mid + 1 else: hi = mid - 1 return -1 def find_min(nums): lo, hi = 0, len(nums) - 1 while lo < hi: mid = (lo + hi) // 2 if nums[mid] > nums[hi]: # минимум справа от mid lo = mid + 1 else: hi = mid return nums[lo] ``` Вторая функция сравнивает mid с правым краем. Если nums[mid] больше nums[hi], точка поворота справа. Иначе она слева или в mid. ## Чем задачи на повёрнутый массив отличаются друг от друга? | Задача | С чем сравнивать mid | Дубликаты | Время | | --- | --- | --- | --- | | Search in Rotated Sorted Array | С nums[lo] и с целью | Нет | O(log n) | | Search in Rotated Sorted Array II | То же, плюс сдвиг при равенстве | Да | O(log n), худший O(n) | | Find Minimum in Rotated Sorted Array | С nums[hi] | Нет | O(log n) | | Найти точку поворота | Индекс минимума | Нет | O(log n) | ## Что меняется, когда есть дубликаты? При nums[lo] равном nums[mid] и nums[hi] непонятно, какая половина отсортирована. Например, [1, 1, 1, 0, 1]. Выход: сдвинуть lo на один вправо и hi на один влево. Потом продолжить. В худшем случае это O(n), но на обычных данных остаётся логарифм. ## По каким словам в задаче узнать приём? - «Отсортированный массив повёрнут в неизвестной точке». - «Найти элемент за O(log n)» в таком массиве. - «Найти минимум» или «сколько раз повернули». - «Массив сначала растёт, потом убывает», это соседняя задача про пик. ## Как найти минимум в повёрнутом массиве? Функция find_min выше. Сравниваем середину с правым краем. Если середина больше, минимум справа, и левую половину вместе с серединой отбрасываем. Иначе минимум слева или в середине, правую половину отбрасываем. Пример [4, 5, 6, 7, 0, 1, 2]. mid = 3, nums[mid] = 7 больше 2, lo = 4. mid = 5, nums[mid] = 1 не больше 2, hi = 5. mid = 4, nums[mid] = 0, hi = 4. Ответ 0. ## На каких ошибках ломается решение? - Сравнивают nums[lo] и nums[mid] через строгое меньше. При lo равном mid половина из одного элемента считается неотсортированной. - Проверяют цель нестрогими границами с обеих сторон и зацикливаются. - В find_min сравнивают с nums[lo] вместо nums[hi]. На неповёрнутом массиве это ломается. - Ищут точку поворота линейно и теряют весь смысл приёма. ## Как объяснить приём на собеседовании? Скажите: одна половина всегда отсортирована. Покажите, как это проверить одним сравнением. Потом скажите, как решить, где цель. Назовите O(log n) и объясните: каждый шаг отбрасывает половину. Упомяните дубликаты как отдельный случай, даже если не спросили. Отработать узнавание приёма можно на странице [бинарный поиск во вращённом массиве](/ru/patterns/binary-search-rotated). Основа лежит на странице [бинарный поиск](/ru/patterns/binary-search-array). ## Вопросы и ответы ### Можно ли сначала найти точку поворота, а потом искать обычным поиском? Да, это два бинарных поиска вместо одного. Работает, тоже O(log n). Один проход короче, но двухшаговый вариант проще объяснить. ### Как найти, на сколько позиций повернули массив? Это индекс минимума. Функция find_min выше возвращает значение, верните вместо него lo. В [4, 5, 6, 7, 0, 1, 2] минимум стоит на индексе 4, значит поворот на 4. ### Где приём встречается в реальных системах? В кольцевых буферах логов, где запись идёт по кругу и самая старая строка лежит в середине. Поиск по времени в таком буфере это та же задача. ### Что делать, если массив не повёрнут? Код выше это учитывает. Левая половина всегда будет отсортирована, и поиск сведётся к обычному. ### Как найти пик в массиве, который растёт, потом убывает? Сравните nums[mid] с nums[mid + 1]. Если справа больше, пик справа. Иначе слева или в mid. Та же идея с другим сравнением. --- title: "Обход графа: BFS или DFS, код на Python и задача Number of Islands" url: https://algopath.pro/ru/blog/obkhod-grafa-bfs-dfs language: ru summary: "Когда брать очередь, а когда стек. Таблица выбора, сигналы в условии, код на Python для списка смежности и для сетки, разбор Number of Islands и типичные ошибки." updated: 2026-10-22 --- # Обход графа: BFS или DFS, код на Python и задача Number of Islands **Короткий ответ.** Обход графа это способ побывать в каждой вершине, до которой можно дойти. BFS идёт по слоям через очередь и находит кратчайший путь в рёбрах. DFS идёт вглубь через стек или рекурсию. Он находит компоненты, циклы и порядок. Оба стоят O(V + E). Ниже таблица выбора, код на Python для списка смежности и сетки, задача Number of Islands. ## Что такое обход графа? Граф это вершины и рёбра. Обход стартует из одной вершины и посещает все достижимые. Каждую вершину посещает один раз, для этого нужен набор visited. Разница между BFS и DFS в порядке. BFS сначала все соседи, потом соседи соседей. DFS сначала один сосед до упора, потом возврат. Граф может быть задан списком смежности, матрицей или сеткой клеток. Код обхода одинаковый, меняется только способ получить соседей. ## Как выбрать между BFS и DFS? | Признак | BFS | DFS | | --- | --- | --- | | Структура | Очередь | Стек или рекурсия | | Кратчайший путь без весов | Да | Нет | | Компоненты, острова | Да | Да, короче в коде | | Поиск цикла, топологический порядок | Неудобно | Да | | Память | Ширина слоя | Глубина пути | | Риск | Большая очередь на широком графе | Переполнение стека на глубоком | Правило простое. Слово «кратчайший» или «минимальное число шагов», берите BFS. Всё остальное, берите DFS. ## По каким словам в задаче узнать обход графа? - «Число островов», «компоненты связности», «заливка области». - «Минимальное число шагов до выхода», «кратчайший путь в лабиринте». - «Можно ли дойти из A в B», «есть ли путь». - «Клетки, соседние по четырём сторонам». Сетка это тоже граф. Клетка это вершина, четыре соседа это рёбра. Многие задачи на матрицы на самом деле про обход графа. ## Как это выглядит на примере? Граф: A связана с B и C. B и C связаны с D. Старт A. BFS: очередь [A]. Снимаем A, кладём B и C. Снимаем B, кладём D. Снимаем C, D уже видели. Снимаем D. Порядок A, B, C, D, расстояния 0, 1, 1, 2. DFS: идём A, потом B, потом D. Из D соседей нет, возврат. Из B больше нет, возврат в A. Идём в C, D уже видели. Порядок A, B, D, C. ## Как написать обход графа на Python? ``` from collections import deque def bfs(graph, start): dist = {start: 0} queue = deque([start]) while queue: v = queue.popleft() for u in graph[v]: if u not in dist: dist[u] = dist[v] + 1 queue.append(u) return dist def dfs(graph, start, visited=None): visited = visited if visited is not None else set() visited.add(start) for u in graph[start]: if u not in visited: dfs(graph, u, visited) return visited def num_islands(grid): rows, cols = len(grid), len(grid[0]) seen = set() def sink(r, c): stack = [(r, c)] while stack: r, c = stack.pop() for nr, nc in ((r+1, c), (r-1, c), (r, c+1), (r, c-1)): if 0 <= nr < rows and 0 <= nc < cols \ and grid[nr][nc] == "1" and (nr, nc) not in seen: seen.add((nr, nc)) stack.append((nr, nc)) count = 0 for r in range(rows): for c in range(cols): if grid[r][c] == "1" and (r, c) not in seen: seen.add((r, c)) sink(r, c) count += 1 return count ``` В num_islands DFS написан через явный стек. На сетке 1000 на 1000 рекурсия упрётся в лимит глубины Python. ## Как решить Number of Islands? Сетка из нулей и единиц. Остров это группа единиц, соседних по сторонам. Нужно число островов. Идём по всем клеткам. Нашли непосещённую единицу, значит новый остров. Запускаем обход из неё и помечаем все клетки острова. Считаем запуски. Каждая клетка посещается один раз. Время O(rows · cols). ## Как BFS находит кратчайший путь? Очередь обрабатывает вершины по возрастанию расстояния. Все вершины на расстоянии 1, потом все на расстоянии 2. Первое попадание в вершину и есть кратчайший путь. Это работает только без весов. С весами нужен алгоритм Дейкстры. ## Как решить задачу про кратчайший путь в лабиринте? Сетка с проходами и стенами, старт и выход. Нужно минимальное число шагов. Это BFS от старта. Каждая клетка получает расстояние при первом попадании в очередь. Дошли до выхода, вернули его расстояние. Очередь пуста, а выхода нет, значит пути нет. ## На каких ошибках ломается обход графа? - Помечают вершину посещённой при снятии из очереди, а не при добавлении. Одна вершина попадает в очередь много раз. - Используют list.pop(0) вместо deque. Каждое снятие стоит O(n). - Рекурсивный DFS на глубоком графе. Лимит Python около тысячи вызовов. - Забывают проверить границы сетки до чтения клетки. ## Как объяснить обход графа на собеседовании? Назовите, чем задан граф и что считать соседом. Скажите, BFS или DFS, и почему. Назовите O(V + E). Объясните: каждая вершина и каждое ребро смотрятся один раз. Для сетки сразу скажите про явный стек или очередь. Это показывает, что вы думали о размере входа. Отработать узнавание приёма можно на странице [обход графа BFS / DFS](/ru/patterns/graph-bfs-dfs). Соседний приём разобран на странице [компоненты связности](/ru/patterns/graph-components). ## Вопросы и ответы ### Как восстановить сам путь после BFS? Храните словарь parent. При добавлении вершины в очередь записывайте, откуда пришли. От цели идите по parent назад до старта. ### Что такое многоисточниковый BFS? В очередь кладут сразу несколько стартовых вершин с расстоянием 0. Так решают «расстояние до ближайшего нуля» за один проход. ### Как найти цикл через DFS? Держите три цвета: белый не был, серый в работе, чёрный закончен. Ребро в серую вершину и есть цикл. В неориентированном графе достаточно проверять, что сосед не родитель. ### Где обход графа встречается в реальных системах? В сборщике мусора, который ищет достижимые объекты. В поисковых роботах, которые идут по ссылкам. В социальных сетях при подсчёте друзей друзей. ### Когда обход графа не подходит? Когда у рёбер есть веса и нужен кратчайший путь. Тогда Дейкстра. И когда граф меняется между запросами, тогда система непересекающихся множеств. --- title: "Динамическое программирование: три шага, код на Python и разбор задач" url: https://algopath.pro/ru/blog/dinamicheskoe-programmirovanie language: ru summary: "Состояние, переход, база. Как узнать приём в задаче, чем таблица отличается от мемоизации, код на Python для Climbing Stairs, Coin Change и House Robber, типичные ошибки." updated: 2026-10-22 --- # Динамическое программирование: три шага, код на Python и разбор задач **Короткий ответ.** Динамическое программирование это способ решать задачу через ответы на её меньшие версии. Ответ для n собирается из ответов для n - 1 и n - 2. Каждый маленький ответ считается один раз и запоминается. Так задачи «сколько способов», «минимальная стоимость», «максимальная выгода» решаются за O(n) или O(n²) вместо перебора. Ниже признаки, три шага, код на Python и разбор задач. ## Что такое динамическое программирование? Берём задачу, у которой большой ответ строится из маленьких. Определяем состояние: что именно значит «ответ для i». Пишем переход: как ответ для i получить из уже посчитанных. Главное условие: маленькие задачи повторяются. Рекурсия без запоминания считала бы их снова и снова. Динамическое программирование считает каждую один раз. Слово «динамическое» тут ничего не значит. Название историческое. Смысл в таблице ответов. ## По каким словам в задаче узнать динамическое программирование? - «Сколькими способами можно», «число путей». - «Минимальная стоимость», «максимальная сумма без соседних». - «Можно ли набрать сумму из монет», «можно ли разбить на два равных». - «Самая длинная возрастающая подпоследовательность». Общий признак: ответ для всего входа зависит от ответов для его частей. И частей мало. Если каждый выбор независим от прошлого, хватит жадного алгоритма. Если нужны все решения, а не лучшее, это бэктрекинг. ## Из каких трёх шагов состоит решение? - Состояние. dp[i] это ответ для первых i элементов или для числа i. - Переход. Формула, которая выражает dp[i] через dp с меньшими индексами. - База. Значения dp для самых маленьких i, которые известны сразу. Потом остаётся порядок вычисления. Считаем от базы к нужному ответу. Тогда каждый переход ссылается на готовые ячейки. ## Как это выглядит на примере? Задача Climbing Stairs: лестница из n ступенек. Шаг на одну или на две. Сколько способов подняться? Состояние: dp[i] это число способов дойти до ступеньки i. Переход: dp[i] = dp[i - 1] + dp[i - 2]. На ступеньку i приходят либо с i - 1, либо с i - 2. База: dp[1] = 1, dp[2] = 2. Для n = 5: dp = [_, 1, 2, 3, 5, 8]. Ответ 8. Это числа Фибоначчи, и это не совпадение. ## Как написать динамическое программирование на Python? ``` def climb_stairs(n): if n <= 2: return n a, b = 1, 2 # dp[i-2], dp[i-1] for _ in range(3, n + 1): a, b = b, a + b return b def coin_change(coins, amount): INF = float("inf") dp = [0] + [INF] * amount # dp[x] = минимум монет на сумму x for x in range(1, amount + 1): for c in coins: if c <= x and dp[x - c] + 1 < dp[x]: dp[x] = dp[x - c] + 1 return -1 if dp[amount] == INF else dp[amount] def rob(houses): take = skip = 0 # лучший результат с последним взятым / пропущенным for h in houses: take, skip = skip + h, max(take, skip) return max(take, skip) ``` Первая функция хранит две переменные вместо массива. Если переход смотрит только на два шага назад, массив не нужен. ## Чем восходящий способ отличается от нисходящего? | Признак | Снизу вверх (таблица) | Сверху вниз (мемоизация) | | --- | --- | --- | | Форма кода | Цикл | Рекурсия с кешем | | Порядок | Нужно выбрать самому | Рекурсия выберет сама | | Память | Часто можно ужать до O(1) | Кеш плюс стек вызовов | | Считает лишнее | Иногда все состояния | Только нужные | | Риск | Ошибка в порядке | Переполнение стека | На собеседовании начинайте с рекурсии и кеша. Так проще найти переход. Потом переписывайте в цикл, если просят. ## Как решить Coin Change? Даны номиналы монет и сумма. Нужно минимальное число монет. Состояние: dp[x] это минимум монет на сумму x. Переход: для каждой монеты c пробуем dp[x - c] + 1. База: dp[0] = 0. Жадный выбор самой большой монеты здесь ломается. Монеты [1, 3, 4], сумма 6. Жадность даёт 4 + 1 + 1, а ответ 3 + 3. Именно поэтому нужно динамическое программирование. ## Как решить House Robber? Ряд домов с деньгами. Нельзя брать два соседних. Нужна максимальная сумма. Состояние: лучший результат для первых i домов. Переход: берём дом i и добавляем результат для i - 2. Или пропускаем и берём результат для i - 1. В коде выше это две переменные take и skip. ## На каких ошибках ломается решение? - Состояние определено размыто, и переход не выражается через него. - База пропущена или неверна, ошибка расходится по всей таблице. - Порядок вычисления такой, что переход читает пустую ячейку. - Берут жадный алгоритм там, где нужен перебор состояний. ## Как объяснить приём на собеседовании? Назовите состояние вслух одной фразой. Потом переход. Потом базу. Только после этого пишите код. Собеседующий проверяет именно эти три фразы. Сложность назовите как число состояний, умноженное на стоимость перехода. Для Coin Change это O(сумма · число монет). Отработать узнавание приёма можно на странице [динамическое программирование](/ru/patterns/dynamic-programming). Нисходящий вариант разобран на странице [рекурсия с мемоизацией](/ru/patterns/memoization). ## Вопросы и ответы ### Как понять, что задача не на жадный алгоритм? Найдите пример, где лучший локальный выбор портит итог. Монеты [1, 3, 4] и сумма 6 такой пример. Нашли, значит нужна таблица. ### Сколько состояний может быть? Обычно до нескольких миллионов. Если состояний больше, ищите другой параметр или другой приём. ### Что делать, если нужен сам ответ, а не число? Храните рядом с dp[i] выбор, который к нему привёл. В конце пройдите по выборам назад и соберите ответ. ### Где динамическое программирование встречается в реальных системах? В выравнивании ДНК и в проверке орфографии через расстояние редактирования. В сжатии данных и в разбиении текста на строки в вёрстке. В маршрутизации, где ищут дешёвый путь по этапам. --- title: "Структуры данных в JavaScript: множество, словарь или массив" url: https://algopath.pro/ru/blog/struktury-dannyh-javascript language: ru summary: "Структуру данных выбирают по вопросу, который ты задаёшь чаще всего. Четыре вопроса, таблица цен и случаи, где массив всё ещё выигрывает." updated: 2026-10-20 --- # Структуры данных в JavaScript: множество, словарь или массив **Короткий ответ.** Структуру выбирай по вопросу, который задаёшь чаще всего. «Видел ли я это раньше?» Множество. «Какое значение у этого ключа?» Словарь. «Какое сейчас минимальное?» Куча. «Что на позиции i, и важен ли порядок?» Массив. Множество и словарь отвечают за один шаг, неотсортированный массив проходит все элементы. ## Четыре вопроса **«Видел ли я это раньше?»** [Множество](https://ru.wikipedia.org/wiki/%D0%9C%D0%BD%D0%BE%D0%B6%D0%B5%D1%81%D1%82%D0%B2%D0%BE_(%D1%82%D0%B8%D0%BF_%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85)). Проверка за один шаг при любом размере. **«Какое значение у этого ключа?»** [Ассоциативный массив](https://ru.wikipedia.org/wiki/%D0%90%D1%81%D1%81%D0%BE%D1%86%D0%B8%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D1%8B%D0%B9_%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2). Та же скорость, и он несёт с собой данные. **«Какое сейчас минимальное?»** [Куча](https://ru.wikipedia.org/wiki/%D0%9A%D1%83%D1%87%D0%B0_(%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85)). Вставка и снятие минимума за log n. **«Что лежит на позиции i, и важен ли порядок?»** Массив. Почти каждая задача первой половины программы это один из четырёх в костюме. Первые два живут в паттерне [хеш-множество и словарь](/ru/patterns/hash-set-map). ## Цена одной и той же операции Найти один элемент: - Неотсортированный массив: шаг на каждый элемент. - Отсортированный массив: log n бинарным поиском. - Множество или словарь: один шаг в среднем. - Куча: один шаг для минимума. Для остального полный проход. Первая строка и превращает решения в квадратичные. Проверка вхождения внутри цикла это два вложенных прохода. На экране выглядит как один. ## Когда массив всё ещё выигрывает Множество не бесплатное. Оно хеширует ключ, идёт по указателю, прыгает в другое место памяти. Примерно до 100 элементов проход по массиву часто быстрее. Современные [кеши процессора](https://ru.wikipedia.org/wiki/%D0%9A%D1%8D%D1%88_%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D1%81%D1%81%D0%BE%D1%80%D0%B0) построены под непрерывную память. Ещё два случая. Массив, индексированный маленькими целыми числами, обходит словарь по всем статьям. Какой вариант разрешён, решают [ограничения входа](/ru/blog/tablitsa-slozhnosti-algoritmov). Порядок, сортировка или «элемент перед этим» требуют массива или дерева. Хеширование уничтожает порядок по своей природе. ## Три ошибки, которые стоит назвать **Словарь там, где хватило бы множества.** Все значения true? Нужно было множество. Множество меньше и говорит то, что ты имел в виду. **Ключи обычного объекта в JavaScript.** Объект превращает любой ключ в строку. Поэтому `1` и `"1"` попадают в одну ячейку. А объект в роли ключа становится «[object Object]». Ключи, которые не строки, требуют [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map). **Удалить не то.** Счётчик упал до нуля, а ключ остался на месте. Нули ломают проверку «есть ли такой ключ» молча. Дошёл до нуля, удаляй ключ. ## Как решить за десять секунд До первой строки кода закончи предложение: «операция, которую я сделаю тысячи раз, это _____». Потом возьми структуру, которая делает эту операцию за один шаг. Скажи это вслух на собеседовании. Выбор структуры с названной причиной и есть то, что проверяют. ## Где это в треке [AlgoPath](/ru) тратит шаги с 11 по 13 на один этот выбор. С кодом, который пишешь ты, до первого именованного паттерна. Ценовая сторона выбора в статье [Big O без математики](/ru/blog/big-o-prostymi-slovami). Первые 33 шага бесплатные. --- title: "Бэктрекинг: шаблон из трёх строк, код на Python и разбор N ферзей" url: https://algopath.pro/ru/blog/bektreking language: ru summary: "Перебор с откатом для задач «все перестановки», «все подмножества» и «N ферзей». Шаблон, код на Python, разбор на примере, отличие от DP и типичные ошибки." updated: 2026-10-17 --- # Бэктрекинг: шаблон из трёх строк, код на Python и разбор N ферзей **Короткий ответ.** Бэктрекинг это перебор с откатом. Мы строим решение по шагам, и как только шаг ведёт в тупик, отменяем его и пробуем другой. Так решают задачи «все перестановки», «все подмножества», «расставить ферзей», «судоку». Ниже шаблон, код на Python, разбор на примере и типичные ошибки. ## Что такое бэктрекинг? Есть частичное решение. На каждом шаге выбираем следующий элемент из допустимых. Если выбор ведёт к полному решению, записываем его. Если выбор ведёт в тупик, откатываем и берём следующий. Это обход дерева вариантов в глубину. Узел это частичное решение, ребро это один выбор. Листья это полные решения или тупики. Слово «откат» описывает главный жест. Сделали шаг, вернулись, сделали другой. Состояние после возврата ровно такое же, как до шага. Отличие от простого перебора: тупики обрезаются рано. Если первые два ферзя бьют друг друга, остальные шесть не расставляем. ## По каким словам в задаче узнать бэктрекинг? - «Все перестановки», «все подмножества», «все сочетания». - «Все способы разбить строку», «все пути в лабиринте». - «Расставить N ферзей», «решить судоку». - «Найти хотя бы одно решение при ограничениях», где n меньше 20. Общий признак: нужен список всех ответов или ответ при жёстких ограничениях, и n маленькое. Если просят только число ответов, чаще подходит динамическое программирование. ## Как устроен шаблон бэктрекинга? - Функция принимает текущее частичное решение path. - Если path полное, записываем копию и выходим. - Для каждого допустимого варианта: добавляем его в path, вызываем функцию, убираем обратно. Три строки «добавить, вызвать, убрать» это и есть откат. Без третьей строки path растёт без остановки. Обрезка тупиков живёт в проверке допустимости варианта перед первой строкой. ## Как это выглядит на примере? Перестановки [1, 2, 3]. path пустой. Берём 1: path [1]. Берём 2: path [1, 2]. Берём 3: path [1, 2, 3], полное, записали. Убираем 3, вариантов нет. Убираем 2. Теперь берём 3: path [1, 3]. Берём 2: [1, 3, 2], записали. Откат до пустого. Берём 2 первым: [2, 1, 3], [2, 3, 1]. Потом [3, 1, 2], [3, 2, 1]. Шесть ответов, ни один не пропущен. ## Как написать бэктрекинг на Python? ``` def permutations(nums): result, path, used = [], [], [False] * len(nums) def go(): if len(path) == len(nums): result.append(path[:]) # копия, а не ссылка return for i, x in enumerate(nums): if used[i]: continue used[i] = True path.append(x) go() path.pop() # откат used[i] = False go() return result def subsets(nums): result, path = [], [] def go(start): result.append(path[:]) for i in range(start, len(nums)): path.append(nums[i]) go(i + 1) path.pop() go(0) return result ``` Параметр start в subsets запрещает возвращаться назад. Так [1, 2] и [2, 1] не считаются дважды. ## Чем бэктрекинг отличается от соседних приёмов? | Приём | Что даёт | Размер n | Пример | | --- | --- | --- | --- | | Бэктрекинг | Все решения или одно при ограничениях | До 15-20 | N ферзей, перестановки | | Динамическое программирование | Число решений или оптимум | До тысяч | Число путей, рюкзак | | Обход в глубину | Достижимость, компоненты | До миллионов | Острова в сетке | | Жадный алгоритм | Оптимум без перебора | Любой | Задача о выборе заявок | ## Как решить N ферзей? Ставим по одному ферзю в каждую строку. Для строки r перебираем столбцы c. Столбец занят или диагональ занята, пропускаем. Держим три множества: занятые столбцы, диагонали r - c и диагонали r + c. Проверка занятости стоит O(1). Дошли до строки n, записали расстановку. Для n = 8 полный перебор дал бы 8 в восьмой степени вариантов. Обрезка тупиков оставляет несколько тысяч. ## Как решить Combination Sum? Даны числа и цель. Нужны все наборы, где числа в сумме дают цель. Число можно брать много раз. Идём по кандидатам с индекса start. Берём число, вычитаем из цели, вызываем себя с тем же start. Цель стала нулём, записываем путь. Цель ушла в минус, откатываем. Сортировка кандидатов позволяет обрывать цикл, когда число больше остатка. ## На каких ошибках ломается бэктрекинг? - Записывают path вместо path[:]. Все ответы становятся одним пустым списком. - Забывают откат после рекурсивного вызова. - Проверяют ограничение только в конце вместо каждого шага. Обрезка не работает. - Не сортируют вход в задачах с дубликатами, и одинаковые ответы повторяются. ## Как объяснить приём на собеседовании? Скажите: нужны все решения, n маленькое, строю ответ по шагам с откатом. Назовите три строки шаблона. Назовите, где обрезаете тупики. Сложность честно называйте экспоненциальной: O(n!) для перестановок, O(2ⁿ) для подмножеств. Потом скажите, что обрезка сильно уменьшает реальное число шагов. Отработать узнавание приёма можно на странице [бэктрекинг](/ru/patterns/backtracking). Соседний приём разобран на странице [рекурсия](/ru/patterns/recursion). ## Вопросы и ответы ### Чем бэктрекинг отличается от обхода в глубину? Механика одна. Разница в том, что бэктрекинг строит решение и отменяет шаг. Обход в глубину просто помечает вершины посещёнными. ### Как убрать дубликаты в подмножествах с повторами? Отсортируйте вход. В цикле пропускайте элемент, если он равен предыдущему и предыдущий не взят на этом уровне. ### Где приём встречается в реальных системах? В решателях ограничений: составление расписаний, раскладка задач по машинам. В парсерах с откатом, где грамматика допускает несколько разборов. В играх, когда движок перебирает ходы на несколько шагов вперёд. ### Когда бэктрекинг слишком медленный? Когда n больше 20 и нет сильной обрезки. Тогда ищите динамическое программирование или жадный подход. --- title: "Префиксные суммы: сумма на отрезке за O(1) и подсчёт подмассивов" url: https://algopath.pro/ru/blog/prefiksnye-summy language: ru summary: "Один проход, и любая сумма отрезка считается за O(1). Устройство, код на Python, словарь для задачи Subarray Sum Equals K, двумерный вариант и типичные ошибки." updated: 2026-10-15 --- # Префиксные суммы: сумма на отрезке за O(1) и подсчёт подмассивов **Короткий ответ.** Префиксные суммы это массив, где pref[i] хранит сумму первых i элементов. После одного прохода сумма любого отрезка считается за O(1): pref[r + 1] - pref[l]. Приём нужен, когда запросов на сумму много, а массив не меняется. С хеш-таблицей он ещё и считает подмассивы с заданной суммой. Ниже устройство, код на Python и разбор задачи Subarray Sum Equals K. ## Что такое префиксные суммы? Берём массив a. Строим массив pref длины n + 1. pref[0] равен 0, pref[i] равен сумме a[0] .. a[i - 1]. Сумма отрезка от l до r включительно это pref[r + 1] - pref[l]. Всё, что до l, вычитается. Остаётся ровно отрезок. Построение стоит O(n). Каждый запрос после этого стоит O(1). Без префиксов каждый запрос стоил бы O(n). При тысяче запросов разница уже в тысячу раз. ## По каким словам в задаче узнать префиксные суммы? - «Много запросов вида сумма на отрезке». - «Сколько подмассивов с суммой k». - «Индекс, где сумма слева равна сумме справа». - «Самый длинный подмассив с суммой 0» или «с равным числом нулей и единиц». Общий признак: сумма отрезка нужна много раз, а массив не меняется. Если массив меняется между запросами, нужно дерево Фенвика. ## Как это выглядит на примере? Массив [2, 4, 1, 3]. pref = [0, 2, 6, 7, 10]. Сумма от индекса 1 до 2, то есть 4 + 1: pref[3] - pref[1] = 7 - 2 = 5. Сумма всего массива: pref[4] - pref[0] = 10. Сумма пустого отрезка от 2 до 1: pref[2] - pref[2] = 0. Лишняя нулевая ячейка в начале убирает особый случай для l = 0. ## Как написать префиксные суммы на Python? ``` from itertools import accumulate def build(a): pref = [0] for x in a: pref.append(pref[-1] + x) return pref # или list(accumulate(a, initial=0)) def range_sum(pref, l, r): return pref[r + 1] - pref[l] # сумма a[l..r] def subarray_sum_equals_k(nums, k): seen = {0: 1} # префикс 0 встречен один раз total = count = 0 for x in nums: total += x count += seen.get(total - k, 0) seen[total] = seen.get(total, 0) + 1 return count ``` Третья функция не хранит массив pref. Ей хватает текущей суммы и словаря, сколько раз каждая сумма встречалась. ## Как префиксные суммы решают Subarray Sum Equals K? Нужно число подмассивов с суммой k. Подмассив от l до r имеет сумму pref[r + 1] - pref[l]. Она равна k, когда pref[l] равен pref[r + 1] - k. Идём слева направо. На каждом шаге смотрим, сколько раз раньше встречался префикс total - k. Столько подмассивов заканчивается здесь. Потом записываем текущий префикс в словарь. Запись {0: 1} в начале учитывает подмассивы с самого начала. Без неё ответ будет меньше на число таких подмассивов. ## Чем префиксные суммы отличаются от соседних приёмов? | Приём | Массив меняется | Запрос | Отрицательные числа | | --- | --- | --- | --- | | Префиксные суммы | Нет | O(1) | Да | | Скользящее окно | Нет | O(n) на весь проход | Нет, условие не монотонно | | Дерево Фенвика | Да | O(log n) | Да | | Массив разностей | Много прибавлений на отрезках | O(1) на обновление | Да | ## Как перенести приём на матрицу? Двумерный префикс pref[i][j] хранит сумму прямоугольника от угла (0, 0) до (i - 1, j - 1). Строится за O(n · m). Сумма прямоугольника считается по формуле включений и исключений. Вычитаем два прямоугольника выше и левее, потом возвращаем пересечение, которое вычли дважды. Четыре чтения, O(1). ## Как решить задачу про подмассив с равным числом нулей и единиц? Замените каждый ноль на минус один. Теперь нужен подмассив с суммой ноль. Идём с текущей суммой и словарём, где сумма встретилась впервые. Повторилась сумма, значит между двумя встречами лежит подмассив с нулевой суммой. Длина это разница индексов. ## На каких ошибках ломается решение? - Массив pref длины n вместо n + 1. Сумма с нулевого индекса требует особого случая. - Записывают текущий префикс в словарь до проверки. Подмассив нулевой длины начинает считаться. - Забывают {0: 1} и теряют подмассивы с начала. - Берут префиксы там, где массив меняется. Каждое изменение стоит O(n) на пересчёт. ## Как объяснить приём на собеседовании? Скажите: много запросов суммы на отрезке, массив не меняется. Назовите pref и формулу разности. Назовите O(n) на построение и O(1) на запрос. Если задача про число подмассивов, добавьте словарь. Скажите: «ищу префикс total - k среди прошлых». Это ключевая фраза. Отработать узнавание приёма можно на странице [префиксные суммы](/ru/patterns/prefix-sums). Соседний приём разобран на странице [массив разностей](/ru/patterns/difference-array). ## Вопросы и ответы ### Можно ли строить префиксы для произведения или XOR? Да, для любой операции с обратной. XOR обратен сам себе. Произведение ломается на нулях, там нужен особый учёт. ### Как найти индекс, где сумма слева равна сумме справа? Посчитайте общую сумму total. Идите по массиву, держите сумму слева. Сумма справа это total минус левая минус текущий элемент. Первое совпадение и есть ответ. Один проход, O(1) памяти. ### Что делать, если массив меняется? Пересчитывать префиксы после каждого изменения слишком дорого. Берите дерево Фенвика: O(log n) на изменение и на запрос. ### Где приём встречается в реальных системах? В аналитике: сумма продаж за любой период из накопленных итогов. В графике: интегральные изображения для быстрого размытия. В базах данных: агрегаты по диапазонам. --- title: "Скользящее окно: как узнать приём в задаче и написать за O(n)" url: https://algopath.pro/ru/blog/skolzyashchee-okno language: ru summary: "Два индекса идут вправо, между ними отрезок со счётчиком. Сигналы в условии, шаблон переменного окна, код на Python, разбор на примере и задача Minimum Window Substring." updated: 2026-10-15 --- # Скользящее окно: как узнать приём в задаче и написать за O(n) **Короткий ответ.** Скользящее окно это два индекса, левый и правый. Оба идут по массиву в одну сторону. Между ними лежит отрезок, и мы держим про него счётчик. Правый край расширяет окно, левый сжимает, когда условие нарушено. Так задачи «самый длинный подмассив с условием» решаются за O(n) вместо O(n²). Ниже сигналы, шаблон, код на Python и разбор. ## Что такое скользящее окно? Окно это непрерывный отрезок массива или строки. Его задают два индекса: left и right. Всё, что между ними, лежит внутри окна. Окно бывает фиксированным: длина задана, и оно просто едет вправо. И бывает переменным: длина меняется по условию. Второй вид встречается чаще и сложнее. Главное свойство: оба индекса только растут. Каждый элемент входит в окно один раз и выходит один раз. Поэтому весь проход стоит O(n). ## По каким словам в задаче узнать скользящее окно? - «Самая длинная подстрока без повторов». - «Наименьший подмассив с суммой не меньше k». - «Максимальная сумма k подряд идущих элементов». - «Есть ли перестановка строки s внутри строки t». Общий признак: речь о непрерывном отрезке. У отрезка есть свойство, которое легко обновить при сдвиге края. Если отрезок может быть не непрерывным, окно не подходит. ## Как устроен шаблон переменного окна? - left = 0. Идём right от 0 до n - 1. - Добавляем элемент right в счётчик. - Пока условие нарушено, убираем элемент left из счётчика и двигаем left вправо. - Теперь окно верное. Обновляем ответ его длиной или содержимым. Внутренний while не делает проход квадратичным. left проходит массив один раз за всю работу. ## Как это выглядит на примере? Строка «abcabcbb», ищем самую длинную подстроку без повторов. right на a, b, c: окно «abc», длина 3. right на второй a: повтор. Двигаем left, пока первая a не выйдет. Окно «bca». right на второй b: повтор. left уходит за первую b. Окно «cab». right на второй c: окно «abc». right на третьей b: окно «cb». right на последней b: окно «b». Ответ 3. ## Как написать скользящее окно на Python? ``` def longest_unique(s): seen = set() left = best = 0 for right, ch in enumerate(s): while ch in seen: # условие нарушено seen.remove(s[left]) left += 1 seen.add(ch) best = max(best, right - left + 1) return best def min_subarray_len(nums, target): left = total = 0 best = float("inf") for right, x in enumerate(nums): total += x while total >= target: # окно годится, пробуем сжать best = min(best, right - left + 1) total -= nums[left] left += 1 return 0 if best == float("inf") else best ``` Во второй функции условие «пока годится» вместо «пока нарушено». Так ищут минимальное окно. Логика та же, меняется только знак. ## Чем скользящее окно отличается от двух указателей? | Признак | Скользящее окно | Два указателя с концов | | --- | --- | --- | | Направление | Оба идут вправо | Идут навстречу | | Что ищем | Непрерывный отрезок | Пару элементов | | Нужна сортировка | Нет | Обычно да | | Состояние | Счётчик по окну | Только два значения | | Пример | Longest Substring Without Repeating | Two Sum II | Ещё три пары, которые путают, и как вести их список, разобраны в статье [два указателя и скользящее окно](/ru/blog/dva-ukazatelya-i-skolzyashchee-okno). ## Как решить Minimum Window Substring? Дана строка s и строка t. Нужно самое короткое окно в s со всеми буквами t. Кратность букв учитывается. Считаем буквы t в словаре need. Двигаем right и уменьшаем need по букве. Заводим счётчик missing, сколько букв ещё не хватает. Когда missing равен нулю, окно годится. Сжимаем left, пока окно годится, и запоминаем лучшее. Каждая буква входит и выходит один раз. Время O(|s| + |t|). ## Как решить задачу про максимальную сумму k подряд идущих? Это фиксированное окно. Считаем сумму первых k элементов. Дальше на каждом шаге прибавляем новый элемент справа и вычитаем ушедший слева. Запоминаем максимум. Пересчёт суммы с нуля на каждом шаге дал бы O(n · k). Окно даёт O(n). Это самый короткий пример, с которого стоит начинать объяснение. ## На каких ошибках ломается скользящее окно? - Сбрасывают left в ноль при нарушении вместо сдвига на один. Получается O(n²). - Обновляют ответ до того, как окно снова стало верным. - Считают длину как right - left вместо right - left + 1. - Берут окно для задачи про подпоследовательность, где элементы могут не идти подряд. ## Как объяснить приём на собеседовании? Скажите: ответ это непрерывный отрезок, свойство отрезка легко обновить при сдвиге края. Назовите два индекса и счётчик. Назовите время O(n) и объясните: каждый индекс идёт только вправо. Потом покажите наивное решение за O(n²). Скажите, что окно убирает пересчёт с нуля. Это и есть довод. Отработать узнавание приёма можно на странице [скользящее окно](/ru/patterns/sliding-window-variable). Версия с фиксированной длиной лежит на странице [окно фиксированного размера](/ru/patterns/sliding-window-fixed). ## Вопросы и ответы ### Работает ли окно с отрицательными числами? Для задач про сумму нет. Условие «сумма не меньше k» перестаёт быть монотонным. Тогда берут префиксные суммы с хеш-таблицей. ### Как хранить счётчик для строк? Словарь буква в число или массив из 26 ячеек для латиницы. Массив быстрее, словарь универсальнее. ### Как понять, что задача про фиксированное окно? В условии есть число k и слова «подряд идущих». Тогда окно всегда длины k, а left равен right - k + 1. ### Что делать, если условие про окно сложное? Держите вспомогательную структуру. Для «не больше k разных букв» это словарь со счётчиками и его размер. Для «максимум в окне» это монотонная очередь. Само движение краёв не меняется. ### Где приём встречается в реальных системах? В ограничителях запросов: сколько запросов пришло за последнюю минуту. В мониторинге: среднее по последним ста измерениям. Это то же окно, только по времени. --- title: "Таблица сложности алгоритмов: что успеет пройти" url: https://algopath.pro/ru/blog/tablitsa-slozhnosti-algoritmov language: ru summary: "Строка с размером входа обычно и решает, какой алгоритм тебе разрешён. Таблица, которая учится за пять минут, и что делать, когда ограничений не дали." updated: 2026-10-15 --- # Таблица сложности алгоритмов: что успеет пройти **Короткий ответ.** Сначала прочитай размер входа: он говорит, какая сложность разрешена. Компьютер делает около 100 миллионов простых операций в секунду. Поэтому при n до 20 можно перебрать все подмножества, до 5000 проходит n², до 100 000 нужен n log n, а около миллиарда нужен логарифм или формула. Всё, что ниже, следует из этого числа и из того, что такое [временная сложность](https://ru.wikipedia.org/wiki/%D0%92%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D0%B0%D1%8F_%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C_%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC%D0%B0). ## Таблица - **n до 10**: можно всё. Перебрать все перестановки это 3,6 миллиона. - **n до 20**: все подмножества это миллион. Битовые маски живут здесь и почти нигде больше. - **n до 500**: куб проходит. [Флойд](https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%A4%D0%BB%D0%BE%D0%B9%D0%B4%D0%B0_%E2%80%94_%D0%A3%D0%BE%D1%80%D1%88%D0%B5%D0%BB%D0%BB%D0%B0), интервальная динамика. - **n до 5000**: квадрат проходит. Все пары, почти вся динамика на строках. - **n до 100000**: нужно n log n. Сортировка, куча, бинарный поиск внутри цикла, сбалансированные деревья. - **n до 10 миллионов**: один проход, и дешёвый. Окна, префиксные суммы, счётчики. - **n около миллиарда**: до каждого элемента дотронуться нельзя. Жди логарифм, формулу или [бинарный поиск](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA) по ответу. Пять минут на заучивание. Убирает большинство неверных поворотов заранее. У последней строки есть своя страница: [бинарный поиск по ответу](/ru/patterns/binary-search-on-answer). ## Читай в обратную сторону Иди от границы к форме кода. n log n обычно означает одну сортировку. Или один цикл с бинарным поиском внутри. Значит идея с двумя вложенными проходами по 100 тысячам мертва до написания. Проверить уже написанную функцию можно в [калькуляторе Big O](/ru/big-o-calculator). Вперёд, ты напишешь квадрат и узнаешь об этом при отправке. Назад, эти полчаса не случаются. ## Другие числа Внимание достаётся размеру входа. Ещё три строки несут столько же. **Диапазон значений.** «Значения до 10⁶» это массив такого размера и прямой подсчёт. «До 10⁹» означает, что нельзя. Нужен словарь или сжатие координат. **Алфавит.** «Строчные латинские буквы» это 26 ячеек. Фиксированный массив заменяет словарь, константа падает. Память становится O(1). Это приятно сказать вслух. **Лимит памяти.** 256 МБ это примерно 64 миллиона четырёхбайтных чисел. Таблица 100 тысяч на 100 тысяч не поместится. Какой бы красивой ни была рекуррента. ## На собеседовании этого обычно не дают В олимпиадных задачах границы написаны. Интервьюеры их чаще всего не называют. Поэтому это один из лучших вопросов, которые можно задать. «Насколько большим бывает массив?» «Значения могут быть отрицательными?» По четыре секунды. Оба меняют ответ. И показывают ровно тот рефлекс, который оценивают. Проговори допущение и продолжай, даже если интервьюер сказал, что неважно. Решение верно при озвученном допущении. Это сильнее, чем тихо ошибаться. Эта идея входит в число пяти идей статьи [почему задачи сложные](/ru/blog/kak-nauchitsya-reshat-algoritmicheskie-zadachi). ## Когда граница выглядит странно Странное ограничение это подсказка. Её оставили тебе. n до 20 при огромном диапазоне значений это задача на битовую маску. Она объявляет себя сама. Маленькое n рядом с кучей запросов значит: посчитай один раз, отвечай быстро. Ровно 10⁵ рядом с лимитом в секунду значит n log n. Ничего медленнее. Эти числа выбраны намеренно. Прочитать их бесплатно. ## Где это тренируется В [AlgoPath](/ru) чтение ограничений стоит на шаге 09 [«Big-O: имя для того, как быстро растёт работа»](/ru/steps/big-o-and-constraints), до первого паттерна. Дальше каждая задача в тренажёре приходит с границами. Именно поэтому. Первые 33 шага бесплатные. Нотация сложности разобрана в статье [Big O без математики](/ru/blog/big-o-prostymi-slovami). --- title: "Алгоритм Дейкстры: кратчайшие пути с кучей за O((V + E) log V)" url: https://algopath.pro/ru/blog/algoritm-deykstry language: ru summary: "Как алгоритм Дейкстры находит кратчайшие пути во взвешенном графе, почему жадный выбор верен, код на Python с heapq, разбор шага и задача Network Delay Time." updated: 2026-10-15 --- # Алгоритм Дейкстры: кратчайшие пути с кучей за O((V + E) log V) **Короткий ответ.** Алгоритм Дейкстры находит кратчайшие пути от одной вершины до всех остальных. Веса рёбер должны быть неотрицательными. Он берёт из кучи ближайшую вершину и улучшает расстояния до её соседей. Время O((V + E) log V). Ниже сигналы в условии, код на Python, разбор шага и задача Network Delay Time. ## Что делает алгоритм Дейкстры? Есть граф, у каждого ребра вес. Нужно узнать длину кратчайшего пути от старта до каждой вершины. Обход в ширину считает путь в рёбрах. Он работает только при одинаковых весах. Дейкстра учитывает веса, но требует, чтобы они были неотрицательными. Идея одна: жадно брать ближайшую ещё не закрытую вершину. Её расстояние уже нельзя улучшить. Через неё пробуем улучшить соседей. Такое улучшение называют релаксацией ребра. ## По каким словам в задаче узнать алгоритм Дейкстры? - «Кратчайший путь», «минимальная стоимость», «минимальное время». - «Взвешенный граф» с неотрицательными весами. - «Сетка, где у каждой клетки своя цена входа». - «Сколько времени сигнал идёт до всех узлов сети». Если все веса равны единице, хватит обхода в ширину. Если есть отрицательные веса, нужен Беллман-Форд. Дейкстра лежит между ними. На собеседованиях это самый частый алгоритм на взвешенных графах. ## Как работает алгоритм по шагам? - Расстояние до старта 0, до остальных бесконечность. - Кладём в кучу пару (0, старт). - Снимаем с кучи пару с наименьшим расстоянием. Если оно больше уже известного, пропускаем. - Для каждого соседа считаем новое расстояние. Если оно меньше записанного, обновляем и кладём в кучу. - Куча пуста, все расстояния готовы. Проверка «больше известного» это ленивое удаление. В куче лежат устаревшие пары, и мы их просто пропускаем. Каждое ребро релаксируется один раз, каждая вершина снимается с кучи не больше раза с актуальным расстоянием. Отсюда и берётся оценка O((V + E) log V). ## Как это выглядит на примере? Вершины A, B, C, D. Рёбра: A-B вес 1, A-C вес 4. Ещё B-C вес 2 и C-D вес 1. Старт A. Снимаем A с расстоянием 0. Сосед B получает 1, сосед C получает 4. Снимаем B с расстоянием 1. Через B до C выходит 3, это меньше 4, обновляем. Снимаем C с расстоянием 3. D получает 4. Снимаем старую пару (4, C), она устарела, пропускаем. Снимаем D. Ответ: A 0, B 1, C 3, D 4. ## Как написать алгоритм Дейкстры на Python? ``` import heapq def dijkstra(graph, start): # graph: {вершина: [(сосед, вес), ...]} dist = {v: float("inf") for v in graph} dist[start] = 0 heap = [(0, start)] while heap: d, v = heapq.heappop(heap) if d > dist[v]: continue # устаревшая запись for u, w in graph[v]: nd = d + w if nd < dist[u]: dist[u] = nd heapq.heappush(heap, (nd, u)) return dist ``` Чтобы восстановить сам путь, храните массив prev. При каждом улучшении записывайте prev[u] = v. Потом идите от цели назад. ## Чем алгоритм Дейкстры отличается от соседних алгоритмов? | Алгоритм | Веса | Время | Когда брать | | --- | --- | --- | --- | | Обход в ширину | Все равны | O(V + E) | Число шагов, лабиринт | | Дейкстра | Неотрицательные | O((V + E) log V) | Дороги, задержки, цены | | Беллман-Форд | Любые | O(V · E) | Есть отрицательные рёбра | | Флойд-Уоршелл | Любые | O(V³) | Между всеми парами, граф маленький | ## Как решить Network Delay Time? Дано n узлов, список рёбер (откуда, куда, время) и стартовый узел. Нужно время, за которое сигнал дойдёт до всех. Строим список смежности, запускаем Дейкстру из старта. Ответ это максимум по всем расстояниям. Если хоть одно осталось бесконечным, сигнал не дойдёт, возвращаем -1. ## На каких ошибках ломается решение? - Запускают алгоритм на отрицательных весах. Ответ будет неверным без предупреждения. - Не пропускают устаревшие пары. Код остаётся верным, но работает медленнее. - Кладут в кучу (вершина, расстояние) вместо (расстояние, вершина). Куча сортирует по первому полю. - Помечают вершину закрытой при добавлении в кучу, а не при снятии. ## Как объяснить алгоритм Дейкстры на собеседовании? Скажите: взвешенный граф, веса неотрицательные, нужны кратчайшие пути от одной вершины. Назовите кучу и жадный выбор ближайшей вершины. Назовите время O((V + E) log V). Дальше объясните, почему жадность верна. Веса неотрицательны, поэтому обходной путь до ближайшей вершины короче быть не может. Это главный вопрос, который задают следом. Отработать узнавание алгоритма можно на странице [алгоритм Дейкстры](/ru/patterns/dijkstra). Обход без весов разобран на странице [обход графа BFS / DFS](/ru/patterns/graph-bfs-dfs). ## Вопросы и ответы ### Почему Дейкстра не работает с отрицательными весами? Закрытая вершина считается окончательной. Отрицательное ребро может потом сделать путь до неё короче. Алгоритм этого уже не увидит. ### Где алгоритм Дейкстры встречается в реальных системах? В навигаторах: перекрёстки это вершины, дороги это рёбра, вес это время проезда. В сетевых протоколах, где маршрутизатор ищет самый дешёвый путь к соседям. В планировщиках, где вес это цена операции. ### Нужно ли множество посещённых вершин? Нет, проверки d больше dist[v] достаточно. Множество можно завести для ясности, но результат тот же. ### Как восстановить сам путь, а не только длину? Заведите словарь prev. При каждом улучшении расстояния до u запишите prev[u] = v. В конце идите от цели по prev до старта и разверните список. Это добавляет O(V) памяти. ### Как найти кратчайший путь в сетке с ценами клеток? Клетка это вершина, соседи это четыре клетки вокруг. Вес ребра это цена клетки, куда входим. Дальше тот же код. --- title: "Бинарный поиск по ответу: приём для задач про минимальную скорость и вместимость" url: https://algopath.pro/ru/blog/binarnyy-poisk-po-otvetu language: ru summary: "Когда ответ не посчитать, но догадку можно проверить, ищите по диапазону ответов. Функция проверки, границы, код на Python и задача про Коко по шагам." updated: 2026-10-08 --- # Бинарный поиск по ответу: приём для задач про минимальную скорость и вместимость **Короткий ответ.** Бинарный поиск по ответу перебирает не массив, а диапазон возможных ответов. Берём кандидата, проверяем за O(n), подходит ли он, и отбрасываем половину диапазона. Приём нужен в задачах «минимальная скорость», «наименьшая вместимость», «максимальное минимальное расстояние». Ниже устройство, функция проверки, код на Python и задача про Коко и бананы. ## Что такое бинарный поиск по ответу? Обычный бинарный поиск ищет число в отсортированном массиве. Здесь массив не отсортирован. Зато сами ответы образуют диапазон от lo до hi. Про каждого кандидата можно спросить: хватает ли его? Ответы на этот вопрос монотонны. До какого-то порога все «нет», после него все «да». Бинарный поиск находит порог. Для метода нужны три вещи: нижняя граница, верхняя граница и функция проверки. Границы задают диапазон, проверка отвечает на вопрос про середину. ## По каким словам в задаче узнать приём? - «Минимальная скорость, чтобы успеть за h часов». - «Наименьшая вместимость корабля, чтобы вывезти груз за d дней». - «Разбить массив на k частей так, чтобы наибольшая сумма была минимальна». - «Расставить k коров в стойлах, чтобы минимальное расстояние было максимальным». Сигнал это слово «минимум» или «максимум» рядом с условием. Проверить фиксированное значение легко. Найти лучшее значение напрямую трудно. Этот разрыв и закрывает приём. Перебор всех кандидатов подряд тоже сработает, но за O(n · R), где R это размер диапазона. ## Как устроена функция проверки? Проверка берёт кандидата и возвращает True, если он удовлетворяет условию. Она должна быть монотонной: раз True, то True и для всех больших кандидатов. Для задач на максимум наоборот. Задача про Коко. Кандидат это скорость k. Часов нужно сумма ceil(куча / k) по всем кучам. Возвращаем True, если сумма не больше h. Быстрее скорость, меньше часов. Монотонно. | Задача | Диапазон | Проверка(x) | Ищем | | --- | --- | --- | --- | | Коко и бананы | 1 .. max(кучи) | часов при скорости x <= h | наименьший True | | Вместимость корабля | max(w) .. sum(w) | дней при вместимости x <= d | наименьший True | | Split Array Largest Sum | max(a) .. sum(a) | частей с лимитом x <= k | наименьший True | | Коровы в стойлах | 1 .. max разрыв | k коров на расстоянии x помещаются | наибольший True | ## Как написать бинарный поиск по ответу на Python? ``` import math def min_eating_speed(piles, h): lo, hi = 1, max(piles) def ok(k): return sum(math.ceil(p / k) for p in piles) <= h while lo < hi: mid = (lo + hi) // 2 if ok(mid): hi = mid # mid подходит, пробуем меньше else: lo = mid + 1 # mid не подходит, нужно больше return lo # вариант на максимум: наибольший x, для которого ok(x) True def largest_ok(lo, hi, ok): while lo < hi: mid = (lo + hi + 1) // 2 if ok(mid): lo = mid else: hi = mid - 1 return lo ``` Цикл держит в lo наименьшее значение, которое ещё может подойти. Когда lo догнал hi, это ответ. Плюс один во втором варианте спасает от бесконечного цикла. ## Как это выглядит на примере? Кучи [3, 6, 7, 11], h = 8. Диапазон от 1 до 11. Пробуем 6: часов 1 + 1 + 2 + 2 = 6, подходит, hi = 6. Пробуем 3: часов 1 + 2 + 3 + 4 = 10, много, lo = 4. Пробуем 5: часов 1 + 2 + 2 + 3 = 8, подходит, hi = 5. Пробуем 4: часов тоже 8, подходит, hi = 4. lo равен hi. Ответ 4. Четыре проверки вместо одиннадцати. При диапазоне до миллиарда будет тридцать проверок вместо миллиарда. ## Как выбрать границы? Нижняя граница это наименьшее осмысленное значение. Для скорости это 1. Для вместимости это самый тяжёлый предмет, иначе он не поедет вообще. Верхняя граница это значение, которое точно подходит. Самая большая куча, сумма всех весов. Лишний запас стоит пары проверок, поэтому берите с запасом. ## На каких ошибках ломается бинарный поиск по ответу? - Проверка не монотонна. Тогда половина с ответом выбрасывается. - Границы не включают ответ. Вместимость меньше самого тяжёлого груза его не увезёт. - mid = (lo + hi) // 2 в задаче на максимум. lo = mid тогда не сдвигается. - Целочисленное деление там, где нужно округление вверх. ## Как объяснить приём на собеседовании? Скажите: «Ответ напрямую не посчитать, но догадку я проверю за O(n)». Потом скажите, что проверка монотонна. Назовите границы. Пишите цикл. Сложность назовите как O(n log(hi - lo)). Логарифм берётся от диапазона ответов, а не от массива. Собеседующие проверяют, что вы знаете, какой это логарифм. Отработать узнавание приёма можно на странице [бинарный поиск по ответу](/ru/patterns/binary-search-on-answer). Версия для массива лежит на странице [бинарный поиск](/ru/patterns/binary-search-array). ## Вопросы и ответы ### Чем это отличается от обычного бинарного поиска? Обычный сравнивает mid с целью в отсортированном массиве. Этот вызывает на mid функцию проверки. Массив вообще может быть не отсортирован. ### Где приём встречается вне собеседований? В планировании нагрузки: сколько серверов нужно, чтобы очередь разгребалась за час. Проверить одно число серверов легко. Найти минимум напрямую сложно. Это та же задача про Коко с другими словами. ### Может ли ответ быть дробным? Да. Крутите цикл фиксированное число раз, около ста, вместо условия lo равен hi. Или остановитесь, когда hi - lo меньше нужной точности. ### Как понять, что проверка монотонна? Спросите себя: если значение x подходит, подойдёт ли x + 1? Для скорости, вместимости и лимита суммы ответ да. Если ответ «не всегда», приём не подходит, и нужен другой способ. ### Что делать, если проверка стоит O(n log n)? Тогда всего будет O(n log n log R). Обычно это проходит. Одна сортировка до цикла часто возвращает проверку к O(n). --- title: "Быстрый и медленный указатели: цикл и середина списка без лишней памяти" url: https://algopath.pro/ru/blog/bystryy-i-medlennyy-ukazateli language: ru summary: "Два указателя со скоростями один и два находят цикл, его начало и середину списка за O(1) памяти. Сигналы в условии, код на Python, доказательство и задача Find the Duplicate Number." updated: 2026-10-06 --- # Быстрый и медленный указатели: цикл и середина списка без лишней памяти **Короткий ответ.** Быстрый и медленный указатели идут по одному списку с разной скоростью. Медленный делает шаг, быстрый два. Если они встретились, в списке есть цикл. Тот же приём находит середину списка за один проход без лишней памяти. Ниже сигналы в условии, код на Python и доказательство для начала цикла. ## Что такое быстрый и медленный указатели? Берём две ссылки на узлы связного списка. Обе стартуют с головы. На каждом шаге медленный сдвигается на один узел, быстрый на два. Если цикла нет, быстрый первым дойдёт до конца. Если цикл есть, быстрый догонит медленного. Они окажутся на одном узле. Приём ещё называют алгоритмом Флойда или «черепаха и заяц». Ему нужна O(1) памяти. В этом его смысл. ## По каким словам в задаче узнать приём? - «Найти цикл в списке» без дополнительной памяти. - «Найти середину списка» за один проход. - «Счастливое число» и любая последовательность, которая может зациклиться. - «Найти повторяющееся число» в массиве со значениями от 1 до n. Общий признак: по данным можно идти только вперёд. Индекса для прыжка нет. Хеш-множество тоже решает, но стоит O(n) памяти. На собеседовании просят вариант за O(1). В реальных системах приём ловит зацикленные ссылки. Например, цепочку редиректов, где страница в итоге ведёт на саму себя. Или список задач, где зависимость замкнулась. ## Как быстрый и медленный указатели находят середину? Медленный шагает по одному, быстрый по два. Когда быстрый упёрся в конец, медленный стоит на середине. При чётной длине середин две. Код ниже возвращает вторую. Чтобы получить первую, остановите быстрый на один узел раньше. ## Как это выглядит на примере? Список 1, 2, 3, 4, 5, и узел 5 ссылается обратно на 3. Шаг 1: медленный на 2, быстрый на 3. Шаг 2: медленный на 3, быстрый на 5. Шаг 3: медленный на 4, быстрый на 4. Встреча, цикл есть. Теперь ищем начало цикла. Медленный возвращается на 1, быстрый остаётся на 4. Оба идут по одному узлу. Шаг: 2 и 5. Шаг: 3 и 3. Встретились на узле 3, это и есть вход в цикл. ## Как написать приём на Python? ``` class Node: def __init__(self, val, next=None): self.val = val self.next = next def middle(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next return slow def has_cycle(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: return True return False def cycle_start(head): slow = fast = head while fast and fast.next: slow, fast = slow.next, fast.next.next if slow is fast: break else: return None slow = head while slow is not fast: slow, fast = slow.next, fast.next return slow ``` ## Почему поиск начала цикла работает? Пусть от головы до входа в цикл a узлов. Длина цикла c. В момент встречи медленный прошёл a + k шагов. Быстрый прошёл вдвое больше. Разница между ними это целое число кругов. Значит a + k делится на c. Ставим один указатель на голову. Ведём оба по одному шагу. Через a шагов первый дойдёт до входа. Второй пройдёт ещё a шагов внутри цикла и тоже окажется на входе. ## Чем приём лучше хеш-множества? | Способ | Время | Память | Находит вход в цикл | | --- | --- | --- | --- | | Множество посещённых узлов | O(n) | O(n) | Да, первый повтор | | Быстрый и медленный указатели | O(n) | O(1) | Да, вторая фаза | | Пометки прямо в узлах | O(n) | O(1) | Да, но портит входные данные | Множество короче в коде. Два указателя берут, когда просят O(1) памяти. ## Как решить Find the Duplicate Number этим приёмом? Массив из n + 1 чисел со значениями от 1 до n. Одно число повторяется. Менять массив и тратить память нельзя. Считаем значение указателем: индекс i ведёт в индекс nums[i]. Два индекса ведут в одно место, значит в таком обходе есть цикл. Повтор это вход в цикл. Запускаем cycle_start на этом обходе. ## На каких ошибках ломается решение? - Проверяют только fast и не проверяют fast.next. На нечётной длине это падение. - Сравнивают значения через ==, а не узлы через is. Два узла могут хранить одно число. - Стартуют быстрый с head.next. Доказательство выше требует общий старт. - Забывают пустой список. Возвращайте ответ сразу, если head равен None. ## Как объяснить приём на собеседовании? Сначала назовите ограничение: O(1) памяти. Потом скорости: один и два. Затем факт: в цикле быстрый догоняет медленного за один круг. И пишите цикл. Спросят про вход в цикл, скажите «вторая фаза, один указатель на голову». Приведите довод про a + k в двух фразах. Этого хватит. Отработать узнавание приёма можно на странице [быстрый и медленный указатели](/ru/patterns/fast-slow-pointers). Соседний приём разобран на странице [разворот связного списка](/ru/patterns/linked-list-reversal). ## Вопросы и ответы ### Почему скорость два, а не три? Для обнаружения цикла годится любая скорость больше единицы. Двойка даёт простое доказательство и короткий путь. Тройка может дольше искать встречу в коротком цикле, но встретится всё равно. ### Как найти длину цикла? После встречи оставьте один указатель на месте. Второй ведите по одному узлу и считайте шаги. Когда он вернётся, счётчик покажет длину цикла. ### Работает ли приём на массивах? Да, когда значение можно читать как следующий индекс. Find the Duplicate Number ровно такой случай. ### Можно ли так найти k-й узел с конца? Это другой приём из той же семьи. Оба указателя идут со скоростью один, между ними зазор в k узлов. --- title: "Big O простыми словами" url: https://algopath.pro/ru/blog/big-o-prostymi-slovami language: ru summary: "Big O отвечает на один вопрос: вход вырос вдвое, что стало с работой? Пять форм и как читать их в своём коде." updated: 2026-10-15 --- # Big O простыми словами **Короткий ответ.** Big O отвечает на один вопрос. Вход вырос вдвое, что стало с работой? При O(1) работа не меняется, при O(log n) растёт на один шаг, при O(n) удваивается, при O(n²) растёт вчетверо. Константы отбрасывают, потому что на больших входах важна только скорость роста. Вся идея Big O в этом. Остальное [нотация](https://ru.wikipedia.org/wiki/%C2%ABO%C2%BB_%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%BE%D0%B5_%D0%B8_%C2%ABo%C2%BB_%D0%BC%D0%B0%D0%BB%D0%BE%D0%B5). ## Пять форм Удвой вход, и: - **O(1)**: работа не меняется. Обращение по индексу. Взятие по ключу. - **O(log n)**: работа выросла на один шаг. По миллиону это 20 шагов. По двум миллионам 21. - **O(n)**: работа удвоилась. Один проход по всему. - **O(n log n)**: чуть больше, чем удвоилась. Любая нормальная сортировка. - **O(n²)**: работа выросла в четыре раза. Все пары. Пять строк. Они покрывают почти всё, что встретится до университетского уровня. Какую из них разрешает размер входа, собрано в [таблице сложности алгоритмов](/ru/blog/tablitsa-slozhnosti-algoritmov). Каждый термин отсюда есть в [глоссарии](/ru/glossary). Полная таблица по структурам данных это [Big-O cheat sheet](https://www.bigocheatsheet.com/). ## Почему константы выбрасывают Цикл, который делает три действия на элемент, это O(n). Ровно как и тот, что делает одно. Ощущается неправильно. В три раза медленнее это в три раза медленнее. Big O отвечает на другой вопрос. Как это ведёт себя при неограниченном росте входа? Тройка остаётся тройкой всегда. Переход от n к n² убегает. При миллионе это 3 миллиона шагов против триллиона. Константа перестаёт быть интересной. ## Как считать по своему коду Три правила закрывают почти всё. **Цикл по входу это n.** Цикл внутри цикла это n². Вложенность умножает. **Деление пополам это log n.** Каждый проход выбрасывает половину оставшегося. Деление пополам работает как бинарный поиск. Поэтому же быстры сбалансированные деревья. **Последовательные шаги складываются, а сложение оставляет большее.** Сортировка плюс проход это n log n плюс n. То есть n log n. Ловушка это скрытый цикл. Метод, который сканирует строку внутри цикла, делает всё квадратичным. Вторых скобок на экране нет. Вызвал что-то внутри цикла, спроси, сколько оно стоит. ## С памятью так же Тот же вопрос про память. Вход вырос вдвое, сколько ты хранишь дополнительно? Пара переменных это O(1). Копия входа это O(n). Таблица всех пар это O(n²). На практике память кончается именно на ней. Интервьюеры спрашивают обе оценки. Ответишь только про время, потеряешь заработанный балл. Сказать оценку памяти это [разговор вслух](/ru/blog/algoritmicheskaya-sektsiya). ## Где нотация врёт Две честные оговорки. На малых размерах побеждают константы. [Сортировка вставками](https://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0_%D0%B2%D1%81%D1%82%D0%B0%D0%B2%D0%BA%D0%B0%D0%BC%D0%B8) обгоняет [быструю](https://ru.wikipedia.org/wiki/%D0%91%D1%8B%D1%81%D1%82%D1%80%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0) примерно до 20 элементов. Настоящие реализации на дне переключаются на неё. Раскладка в памяти тоже важна. Проход по массиву быстрее прохода по списку той же длины. Оба O(n). Один лежит в [кеше](https://ru.wikipedia.org/wiki/%D0%9A%D1%8D%D1%88_%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D1%81%D1%81%D0%BE%D1%80%D0%B0), другой нет. Big O выбирает алгоритм. Измерение выбирает реализацию. Нужны оба, и отвечают они на разные вопросы. [Калькулятор Big O](/ru/big-o-calculator) замерит твою функцию на растущих входах и назовёт её класс. ## Где это в треке В [AlgoPath](/ru) сложность стоит на шаге 08 [«Почему одни способы быстрее»](/ru/steps/why-some-ways-are-faster), до всех паттернов. Дальше каждый шаг называет свою цену и ждёт, что ты её прочитаешь. Почему это одна из пяти недостающих вещей, разобрано в статье [почему задачи сложные](/ru/blog/kak-nauchitsya-reshat-algoritmicheskie-zadachi). Первые 33 шага бесплатные. --- title: "Префиксное дерево (trie): когда set не хватает и как написать за 20 строк" url: https://algopath.pro/ru/blog/prefiksnoe-derevo language: ru summary: "Префиксное дерево хранит слова по буквам и отвечает про префиксы за O(L). Чем оно отличается от хеш-множества, код на Python и задача Word Search II." updated: 2026-10-15 --- # Префиксное дерево (trie): когда set не хватает и как написать за 20 строк **Короткий ответ.** Префиксное дерево, оно же trie, хранит слова по буквам. Общий префикс хранится один раз. Поиск слова или префикса стоит O(L), где L это длина слова. Его берут для автодополнения, проверки префиксов и задач вида Word Search II. Ниже устройство, код на Python и типичные ошибки. ## Что такое префиксное дерево? Корень дерева пустой. Каждое ребро подписано одной буквой. Путь от корня до узла это префикс. Узел с флагом «конец» значит, что здесь заканчивается слово. Слова «кот», «код» и «кол» дают ветку «ко» и три листа. Общее начало хранится один раз. Хеш-множество отвечает «есть ли слово». Дерево отвечает ещё и «есть ли слова с таким началом». В этом вся разница. ## Когда задаче нужно префиксное дерево, а не set? - «Автодополнение», «подсказки по первым буквам». - «Есть ли слово с таким префиксом», «сколько слов начинается на». - «Найти все слова из словаря в сетке букв». - «Самый длинный общий префикс» для большого словаря. Если в задаче нет слова «префикс» и нет перебора по буквам, хватит set. Дерево сложнее в коде, и без нужды его не пишут. Лишняя структура на собеседовании выглядит хуже, чем простая. | Операция | Хеш-множество | Префиксное дерево | | --- | --- | --- | | Есть ли слово | O(L) в среднем | O(L) всегда | | Есть ли префикс | O(N · L), перебор всех | O(L) | | Все слова с префиксом | O(N · L) | O(L + размер ответа) | | Память | Каждое слово целиком | Общие префиксы один раз, но узлы тяжёлые | ## Как устроен узел дерева? Узел это словарь детей и флаг конца слова. Ключ словаря это буква, значение это следующий узел. Для алфавита из 26 букв вместо словаря берут массив из 26 ссылок. Так быстрее, но памяти уходит больше. Иногда в узел кладут счётчик. Он хранит, сколько слов проходит через узел. Тогда вопрос «сколько слов начинается на ко» решается за два шага. Без счётчика пришлось бы обходить поддерево. ## Как выглядит поиск на примере? В дереве лежат слова «кот», «код» и «кола». Ищем «кол». Из корня идём по «к», потом по «о», потом по «л». Узел есть, но флага end на нём нет. Ответ: слова нет, а префикс есть. Ищем «кит». Из корня идём по «к», дальше ребёнка «и» нет. Выходим сразу. На это ушло два шага, а не проход по всему словарю. Вставляем «кора». «к» и «о» уже есть, создаём «р» и «а». Ставим флаг end на последнем узле. Два новых узла вместо четырёх. ## Как написать префиксное дерево на Python? ``` class Node: __slots__ = ("children", "end") def __init__(self): self.children = {} self.end = False class Trie: def __init__(self): self.root = Node() def insert(self, word): node = self.root for ch in word: node = node.children.setdefault(ch, Node()) node.end = True def _walk(self, prefix): node = self.root for ch in prefix: node = node.children.get(ch) if node is None: return None return node def search(self, word): node = self._walk(word) return node is not None and node.end def starts_with(self, prefix): return self._walk(prefix) is not None ``` Три метода стоят O(L). Вставка создаёт не больше L новых узлов. ## Как решить Word Search II через префиксное дерево? Дана сетка букв и список слов. Нужно найти все слова, которые читаются по соседним клеткам. Без дерева пришлось бы искать каждое слово отдельно. Это перебор для каждого слова с нуля. С деревом делаем один обход в глубину из каждой клетки. Идём по сетке и одновременно спускаемся по дереву. Нет ребёнка с такой буквой, значит ветка тупиковая, и мы выходим. Дошли до узла с флагом end, записываем слово. Так все слова ищутся за один обход. ## На каких ошибках ломается префиксное дерево? - search возвращает True на префиксе. Забыли проверить флаг end. - Флаг end ставят на каждом узле пути, а не на последнем. - В Word Search II не помечают посещённые клетки и читают одну букву дважды. - Строят дерево на каждый запрос заново вместо одного раза. - Берут дерево там, где хватило бы set, и тратят время на лишний код. ## Как объяснить приём на собеседовании? Скажите: нужны ответы про префиксы, set их не даёт. Опишите узел: словарь детей плюс флаг. Назовите время O(L) на все три операции. Потом сравните с сортированным списком и бинарным поиском. Он тоже находит префикс, но стоит O(log N · L), а вставка в него дорогая. Узнавать приём среди соседних можно на странице [префиксное дерево](/ru/patterns/trie). Общая карта приёмов лежит в статье [паттерны алгоритмов](/ru/blog/patterny-algoritmov). ## Вопросы и ответы ### Сколько памяти занимает дерево? В худшем случае столько же узлов, сколько букв во всех словах. Каждый узел это объект со словарём, то есть десятки байт. Для миллионов слов это заметно. ### Как удалить слово из дерева? Снять флаг end. Затем подниматься от листа и удалять узлы без детей и без флага. Чаще всего хватает первого шага. ### Как найти все слова с данным префиксом? Дойдите до узла префикса методом _walk. Потом обойдите его поддерево в глубину. Каждый узел с флагом end даёт слово. Для автодополнения обход обрывают после первых десяти найденных. Тогда ответ приходит быстро даже на большом словаре. ### Чем отличается сжатое дерево? Цепочку узлов с одним ребёнком склеивают в одно ребро с подстрокой. Так устроены маршрутизаторы URL. На собеседовании это спрашивают редко. --- title: "Система непересекающихся множеств: union-find за O(1) и когда его брать" url: https://algopath.pro/ru/blog/sistema-neperesekayushchikhsya-mnozhestv language: ru summary: "Union-find отвечает, лежат ли два элемента в одной группе, почти за O(1). Массив parent, сжатие пути, код на Python и задача Number of Provinces." updated: 2026-10-01 --- # Система непересекающихся множеств: union-find за O(1) и когда его брать **Короткий ответ.** Система непересекающихся множеств хранит элементы группами и отвечает на один вопрос: лежат ли два элемента в одной группе. Две операции, find и union, работают почти за O(1). Её берут в задачах про соединённые города, друзей и компоненты графа. Ниже устройство, код на Python и задача Number of Provinces. ## Что такое система непересекающихся множеств? Есть n элементов. Каждый лежит ровно в одной группе. Группы можно сливать, а разделять нельзя. У каждой группы есть представитель. Это один из её элементов, его называют корнем. Два элемента в одной группе, если у них общий корень. Английское название union-find. В русских статьях встречается сокращение СНМ. Всё это одно и то же. ## Как система непересекающихся множеств хранится в памяти? Один массив parent. В parent[i] лежит родитель элемента i. Корень указывает сам на себя. Группа выглядит как дерево. Чтобы узнать корень, идём по родителям вверх. Чтобы слить группы, подвешиваем корень одной под корень другой. | Приём | Что делает | Глубина дерева | | --- | --- | --- | | Без ускорений | Подвешиваем как попало | До n, то есть O(n) на запрос | | Сжатие пути | При find все узлы на пути переподвешиваем к корню | Почти константа в среднем | | Объединение по рангу | Меньшее дерево под большее | Не больше log n | | Оба приёма | Стандартная реализация | Обратная функция Аккермана, на практике O(1) | ## По каким словам в задаче узнать этот приём? - «Соединены ли города», «сколько групп друзей», «число компонент». - «Добавляем рёбра по одному и после каждого отвечаем на вопрос». - «Найти ребро, которое создаёт цикл». - «Минимальное остовное дерево», алгоритм Крускала. Ключевой признак: рёбра только добавляются. Если рёбра ещё и удаляют, приём не подходит. Тогда нужен обход графа заново. ## Как выглядит слияние на примере? Пять элементов, parent = [0, 1, 2, 3, 4]. Каждый сам себе корень, групп пять. union(0, 1): корни 0 и 1, подвешиваем 1 под 0. parent = [0, 0, 2, 3, 4], групп четыре. union(2, 3): parent = [0, 0, 2, 2, 4], групп три. union(1, 3): find(1) даёт 0, find(3) даёт 2. Подвешиваем 2 под 0: parent = [0, 0, 0, 2, 4]. Групп две. Теперь find(3) идёт 3, 2, 0. Сжатие пути перепишет parent[3] в 0, и следующий запрос дойдёт за один шаг. union(0, 3) вернёт False. Оба корня уже 0, слияния не было. Ровно так ловят лишнее ребро в задачах про циклы. ## Как написать систему непересекающихся множеств на Python? ``` class DSU: def __init__(self, n): self.parent = list(range(n)) self.rank = [0] * n self.groups = n def find(self, x): while self.parent[x] != x: self.parent[x] = self.parent[self.parent[x]] # сжатие пути x = self.parent[x] return x def union(self, a, b): ra, rb = self.find(a), self.find(b) if ra == rb: return False # уже в одной группе if self.rank[ra] < self.rank[rb]: ra, rb = rb, ra self.parent[rb] = ra if self.rank[ra] == self.rank[rb]: self.rank[ra] += 1 self.groups -= 1 return True ``` Метод union возвращает False, если элементы уже были вместе. Так удобно ловить цикл: ребро между вершинами одной группы замыкает его. ## Как решить Number of Provinces через этот приём? Дана матрица n на n. Единица в клетке (i, j) значит, что города i и j соединены. Нужно число групп городов. Создаём DSU на n элементов. Проходим по матрице. Для каждой единицы делаем union(i, j). Ответ лежит в поле groups. Сложность O(n²) на проход по матрице. Сами операции почти бесплатны. Обход в глубину дал бы тот же результат, но union-find короче в коде. ## Где система непересекающихся множеств выигрывает у обхода графа? Обход в глубину считает компоненты за один проход. Если граф не меняется, этого достаточно. Union-find выигрывает, когда рёбра приходят потоком. После каждого нового ребра ответ обновляется за O(1). Обход пришлось бы запускать заново, а это O(V + E) на каждый запрос. ## На каких ошибках ломается решение? - Сравнивают parent[a] и parent[b] вместо find(a) и find(b). Родитель это не корень. - Забывают сжатие пути и получают деревья глубиной n. - Считают группы отдельным проходом вместо счётчика в union. - Индексируют города с единицы, а массив с нуля. ## Как объяснить приём на собеседовании? Скажите: нужно быстро отвечать «в одной ли группе», пока группы сливаются. Назовите две операции и массив parent. Упомяните сжатие пути и ранг, назовите почти константное время. Потом покажите, что union возвращает False на цикле. Это частый следующий вопрос. Отработать узнавание приёма среди соседних можно на странице [система непересекающихся множеств](/ru/patterns/union-find). Соседний приём разобран на странице [компоненты связности](/ru/patterns/graph-components). ## Вопросы и ответы ### Можно ли удалить элемент из группы? В базовой структуре нет. Нужен либо обход графа заново, либо обратный порядок: добавлять рёбра с конца. ### Нужен ли ранг, если есть сжатие пути? Сжатия пути одного хватает для задач на собеседовании. Ранг даёт строгую оценку log n на худший запрос. На практике пишут оба. ### Как посчитать размер каждой группы? Заведите массив size, в начале все единицы. При слиянии прибавляйте размер меньшего дерева к большему. Размер группы элемента x лежит в size[find(x)]. Тот же массив можно взять вместо ранга: подвешивать меньшую группу под большую. ### Как хранить строки вместо чисел? Заведите словарь строка в номер. Номера выдавайте по мере появления. Дальше работает та же структура. --- title: "Бинарная куча: как устроена, когда нужна и как написать на Python" url: https://algopath.pro/ru/blog/binarnaya-kucha language: ru summary: "Бинарная куча хранит минимум в корне и меняется за O(log n). Как она лежит в массиве, по каким словам её узнать в задаче, heapq на Python и задача Kth Largest Element." updated: 2026-09-29 --- # Бинарная куча: как устроена, когда нужна и как написать на Python **Короткий ответ.** Бинарная куча это массив, который ведёт себя как дерево. Корень всегда хранит минимум или максимум. Вставка и удаление стоят O(log n), а взять минимум можно за O(1). Её берут, когда задача просит «k самых» или «следующий по порядку». Ниже устройство, код на Python и типичные ошибки. ## Что такое бинарная куча? Куча это полное двоичное дерево с одним правилом. Каждый родитель не больше своих детей. Такая куча называется min-heap. Если родитель не меньше детей, это max-heap. Дерево при этом никто не строит из узлов. Оно лежит в обычном массиве. Это главная причина, почему куча быстрая и экономная. На собеседовании куча почти всегда появляется под именем «очередь с приоритетом». Это одна и та же структура. ## Как бинарная куча хранится в массиве? Корень лежит в позиции 0. Дети элемента i лежат в позициях 2i + 1 и 2i + 2. Родитель элемента i лежит в позиции (i - 1) // 2. Никаких указателей. Переход к ребёнку это умножение, переход к родителю это деление. Уровни дерева заполняются слева направо без дыр. | Операция | Время | Что происходит | | --- | --- | --- | | Посмотреть минимум | O(1) | Читаем позицию 0 | | Вставить | O(log n) | Кладём в конец, поднимаем вверх | | Удалить минимум | O(log n) | Последний в корень, опускаем вниз | | Построить из массива | O(n) | Опускаем каждый узел с середины к началу | | Найти произвольный элемент | O(n) | Куча этого не умеет | ## По каким словам в задаче узнать кучу? - «k наибольших», «k ближайших», «k самых частых». - «Слить k отсортированных списков». - «Медиана потока» или «текущий минимум при постоянных вставках». - «Планировщик задач», «следующее событие по времени». Общий признак один. Данные приходят или меняются, а нужен всё время самый маленький или самый большой. Полная сортировка на каждом шаге стоит O(n log n). Куча делает то же за O(log n). ## Как работают подъём и спуск? - Вставка: новый элемент идёт в конец массива. Пока он меньше родителя, меняем их местами. Это подъём. - Удаление минимума: корень запоминаем, на его место ставим последний элемент. Пока он больше кого-то из детей, меняем с меньшим ребёнком. Это спуск. Высота полного дерева равна log n. Подъём и спуск проходят не больше одной ветки. Отсюда O(log n). ## Как выглядит вставка на примере? Возьмём min-heap [1, 3, 2, 7, 5]. Вставляем 0. Он идёт в конец: [1, 3, 2, 7, 5, 0]. Его индекс 5, родитель в позиции 2, там лежит 2. 0 меньше 2, меняем: [1, 3, 0, 7, 5, 2]. Теперь индекс 2, родитель в позиции 0, там лежит 1. 0 меньше 1, меняем ещё раз: [0, 3, 1, 7, 5, 2]. Подъём закончен за два шага. Удаление минимума идёт в обратную сторону. Снимаем 0, ставим на его место 2, и 2 опускается вниз к своему уровню. На каждом шаге выбираем меньшего ребёнка. Иначе правило кучи нарушится. ## Как написать бинарную кучу на Python? В Python есть модуль heapq. Он работает поверх обычного списка и делает min-heap. ``` import heapq nums = [5, 1, 8, 3, 2] heapq.heapify(nums) # O(n), nums[0] == 1 heapq.heappush(nums, 0) # O(log n) smallest = heapq.heappop(nums) # 0 # max-heap: храним числа со знаком минус top = [] for x in [5, 1, 8]: heapq.heappush(top, -x) largest = -heapq.heappop(top) # 8 # k наименьших за O(n log k) def k_smallest(nums, k): heap = [] for x in nums: heapq.heappush(heap, -x) if len(heap) > k: heapq.heappop(heap) return sorted(-x for x in heap) ``` Приём с минусом стоит запомнить. Второй способ это кортежи вида (приоритет, значение). Кортежи сравниваются по первому полю. ## Как решить задачу Kth Largest Element через кучу? Условие: найти k-й по величине элемент массива. Сортировка даёт O(n log n). Куча даёт O(n log k). Держим min-heap размера k. Каждый элемент кладём в кучу. Если размер стал k + 1, снимаем минимум. В конце в корне лежит ответ. Почему это работает. Куча всегда хранит k самых больших из просмотренных. Самый маленький из них и есть k-й по величине. ## На каких ошибках ломается решение с кучей? - Кучу используют как отсортированный список. Отсортирован только корень, остальные элементы лежат в произвольном порядке. - Забывают про минус для max-heap и получают k наименьших. - Кладут в кучу несравнимые объекты. Нужен кортеж с числом в первом поле. - Ищут элемент в куче линейным проходом и считают, что это O(log n). ## Как объяснить кучу на собеседовании? Скажите, что нужен быстрый доступ к минимуму при постоянных вставках. Назовите время операций из таблицы выше. Покажите формулу детей 2i + 1 и 2i + 2. Потом назовите альтернативу. Сортировка проще, но стоит O(n log n) на каждое изменение. Это и есть довод в пользу кучи. Тренировать узнавание кучи среди соседних приёмов можно на странице [бинарная куча](/ru/patterns/binary-heap). Общий список приёмов лежит в статье [паттерны алгоритмов](/ru/blog/patterny-algoritmov). ## Вопросы и ответы ### Чем куча отличается от двоичного дерева поиска? В дереве поиска слева всё меньше, справа всё больше. В куче только родитель меньше детей. Поэтому куча не умеет искать, зато строится за O(n). ### Почему построение кучи стоит O(n), а не O(n log n)? Половина узлов это листья, их опускать не надо. Чем выше узел, тем их меньше. Сумма всех спусков сходится к линейной. ### Когда куча хуже сортировки? Когда данные не меняются и нужны все элементы по порядку. Тогда одна сортировка проще и быстрее по константе. --- title: "Как научиться решать алгоритмические задачи" url: https://algopath.pro/ru/blog/kak-nauchitsya-reshat-algoritmicheskie-zadachi language: ru summary: "Знать язык и решать алгоритмические задачи это два разных навыка. Между ними лежат пять конкретных вещей, и ни одна из них не талант." updated: 2026-10-15 --- # Как научиться решать алгоритмические задачи **Короткий ответ.** Знать язык и решать алгоритмические задачи это два разных навыка. Второй стоит на пяти идеях, которые никто не перечисляет. Сколько стоит операция. Какая структура подходит под вопрос. Какой инвариант держит цикл. Как прогнать пример руками. Как читать ограничения. Каждая идея умещается в абзац, все пять в неделю. ## 1. Сколько стоит операция Ты умеешь взять элемент массива. Вопрос в том, сколько это стоит. Обращение по индексу это один шаг при любом размере. Поиск в неотсортированном массиве это шаг на каждый элемент. Взять значение по ключу в [ассоциативном массиве](https://ru.wikipedia.org/wiki/%D0%90%D1%81%D1%81%D0%BE%D1%86%D0%B8%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D1%8B%D0%B9_%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2) это один шаг. Сортировка это n log n. Пять фактов. Без них выбор алгоритма это подбрасывание монеты. Нельзя сравнить два плана, у которых нет цены. Как их оценивать, разобрано в статье [Big O простыми словами](/ru/blog/big-o-prostymi-slovami). ## 2. Выбор структуры Новички держат всё в массивах, потому что массивам их и учили. Спрашивать надо другое: что ты будешь делать чаще всего. «Видел ли я это раньше» указывает на [множество](https://ru.wikipedia.org/wiki/%D0%9C%D0%BD%D0%BE%D0%B6%D0%B5%D1%81%D1%82%D0%B2%D0%BE_(%D1%82%D0%B8%D0%BF_%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85)). «Какое значение у этого ключа» на словарь. «Дай самое маленькое из виденных» на [кучу](https://ru.wikipedia.org/wiki/%D0%9A%D1%83%D1%87%D0%B0_(%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85)). Важен порядок, оставляй массив. Половина задач решается правильным выбором здесь. Остальной код дописывается сам. ## 3. Инвариант [Инвариант](https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%B2%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82_%D1%86%D0%B8%D0%BA%D0%BB%D0%B0) это одно предложение, которое верно на каждом проходе цикла. «Всё слева от i уже отсортировано». «Сумма окна всегда не больше k». Цикл без такого предложения пишут перебором. Добавил единицу, тест прошёл. Почему, ты так и не узнал. Пиши предложение до цикла. Большинство ошибок на единицу исчезает. Плюс один либо сохраняет предложение верным, либо нет. ## 4. Прогнать пример руками Возьми четыре элемента и реши на бумаге. Медленно. Потом посмотри, что делала рука. Сравнила две вещи, запомнила лучшее, пошла дальше. Эти три шага и есть алгоритм. Почти любая техника из первой половины программы формализует то, что люди делают руками. Пропустил шаг, и код выглядит взявшимся из ниоткуда. ## 5. Читать ограничения как инструкцию «n до 1000» разрешает квадрат. «n до 200000» его запрещает. Одна строка часто определяет весь подход. Новички читают её как мелочь. Полная таблица границ собрана в статье [таблица сложности алгоритмов](/ru/blog/tablitsa-slozhnosti-algoritmov). ## Чего здесь нет Ни одна из пяти вещей не про интеллект и не про математику. Пять вещей умещаются в пять маленьких кусков знания, каждый на абзац. Туториалы считают, что они у тебя есть. Любой, кто полгода писал рабочий код, укладывает все пять в неделю. Невозможным это кажется из-за порядка подачи. Тебе дали сороковой шаг под видом пятого. ## Где начинается лестница [AlgoPath](/ru) начинается до первого алгоритма, с бесплатного урока [что такое алгоритм](/ru/steps/what-is-an-algorithm). Цена операций, структуры, инварианты, ограничения. По шагу на каждое, с кодом, который пишешь ты. Первые 33 шага бесплатные, и все пять вещей внутри них. Навык поверх этих пяти это [распознавание паттернов](/ru/blog/patterny-algoritmov). Спутанные пары перечислены в статье [какие паттерны путают](/ru/blog/dva-ukazatelya-i-skolzyashchee-okno). Если материал не столько сложный, сколько неприятный, это [другая проблема с другим лечением](/ru/blog/nenavizhu-algoritmy). --- title: "Алгоритм Кана: топологическая сортировка простыми словами" url: https://algopath.pro/ru/blog/algoritm-kana language: ru summary: "Алгоритм Кана строит топологический порядок через подсчёт входящих рёбер. Трассировка по шагам, код на Python с проверкой на цикл, слова в условии, по которым его узнают, и три ошибки." updated: 2026-09-26 --- # Алгоритм Кана: топологическая сортировка простыми словами **Короткий ответ.** Алгоритм Кана это топологическая сортировка через подсчёт входящих рёбер. Берём вершины, в которые ничего не ведёт, снимаем их, уменьшаем счётчики соседей. Повторяем, пока есть что снимать. Если сняли не все вершины, в графе цикл. Ниже код на Python, где он нужен и на чём он ломается. ## Что делает топологическая сортировка? Она выстраивает вершины ориентированного графа в ряд. Каждое ребро в нём идёт слева направо. Пример из жизни. Курсы в университете и их зависимости. Топологический порядок это порядок, в котором их можно пройти. Работает только на графе без циклов. Если A требует B, а B требует A, порядка нет. ## Как работает алгоритм Кана? Четыре шага, и все они простые. - Для каждой вершины считаем, сколько рёбер в неё входит. Это in-degree. - Все вершины с in-degree 0 кладём в очередь. - Снимаем вершину из очереди, добавляем в ответ. У всех её соседей уменьшаем in-degree на 1. - Сосед дошёл до нуля, кладём его в очередь. Проследим на графе с рёбрами A→C, B→C, C→D. | Шаг | Очередь | Снимаем | Ответ | | --- | --- | --- | --- | | 0 | A, B | | | | 1 | B | A | A | | 2 | C | B | A, B | | 3 | D | C | A, B, C | | 4 | | D | A, B, C, D | C попал в очередь только на шаге 2. До этого в него вели два ребра, и оба надо было снять. ## Как написать алгоритм Кана на Python? Граф задан списком рёбер. Вершины пронумерованы от 0 до n-1. ``` from collections import deque def kahn(n, edges): graph = [[] for _ in range(n)] indegree = [0] * n for a, b in edges: graph[a].append(b) indegree[b] += 1 queue = deque(v for v in range(n) if indegree[v] == 0) order = [] while queue: v = queue.popleft() order.append(v) for w in graph[v]: indegree[w] -= 1 if indegree[w] == 0: queue.append(w) if len(order) != n: return None # в графе есть цикл return order ``` Проверка на цикл встроена. Вершина из цикла никогда не дойдёт до нуля. В ответ она не попадёт. Сложность O(V + E). Каждую вершину снимаем один раз, каждое ребро уменьшаем один раз. ## Когда алгоритм Кана нужен в задаче? Слова в условии, по которым его узнают. - «Курсы и пререквизиты», «можно ли пройти все курсы». - «Порядок сборки», «зависимости пакетов», «порядок задач». - «Найди любой порядок, в котором…». - «Есть ли цикл в ориентированном графе». - «Восстанови алфавит по отсортированным словам». Общий признак: направленные зависимости. Нужен порядок, или проверка, что он существует. Сигналы, соседи паттерна и шаблоны на шести языках на странице [топологической сортировки](/ru/patterns/topological-sort). ## Чем алгоритм Кана отличается от DFS-варианта? Топологическую сортировку делают двумя способами. Оба дают верный порядок. - Кан идёт от вершин без входящих рёбер. Порядок строится с начала. - DFS идёт вглубь и записывает вершину при выходе. Ответ переворачивают. - Кан находит цикл по счётчику. DFS находит цикл по серой вершине. - Кан удобнее, когда нужен порядок «по слоям». Например, задачи, которые можно делать параллельно. На собеседовании чаще ждут Кана. Его проще объяснить вслух, а это половина оценки. ## Как решить Course Schedule алгоритмом Кана? Условие. Есть n курсов. Пары вида «сначала b, потом a». Можно ли пройти все? Читаем как граф. Каждая пара это ребро b→a. Вопрос: есть ли в графе цикл. Запускаем `kahn`. Вернул порядок, ответ «да». Вернул None, ответ «нет». Course Schedule II просит сам порядок. Тот же код, возвращаем список вместо да или нет. Проговорим маленький случай. n = 3, пары (1, 0) и (2, 1). Входящие: 0, 1, 1. В очереди сначала 0. Сняли 0, освободилась 1. Сняли 1, освободилась 2. Порядок: 0, 1, 2. Добавим пару (0, 2). У всех вершин по одному входящему ребру. Очередь пуста с самого начала, ответ «нет». ## Как объяснить алгоритм Кана на собеседовании? Три фразы до кода, в таком порядке. - «Зависимости это ориентированный граф. Нужен топологический порядок.» - «Считаю входящие рёбра и начинаю с вершин без них. Снятая вершина освобождает соседей.» - «Каждая вершина и каждое ребро трогаются один раз, O(V + E). Порядок короче n, значит цикл.» Потом код. Потом трассировка на трёх вершинах. Сложность в конце ещё раз. ## На чём ломается алгоритм Кана? Три ошибки, которые встречаются чаще других. - Ребро прочитано в обратную сторону. Порядок выходит зеркальный, тесты красные. - Забыта проверка `len(order) != n`. Граф с циклом даёт неполный ответ без ошибки. - Вместо очереди взят список с `pop(0)`. Это O(n) на каждый шаг. ## Как закрепить алгоритм Кана? Напиши `kahn` по памяти. Потом реши Course Schedule и Course Schedule II. Это одна и та же задача, вторая просит порядок. Через день напиши снова без подглядывания. Как это работает, объяснено в статье про [интервальные повторения](/ru/blog/intervalnye-povtoreniya). ## Вопросы и ответы ### Какая сложность у алгоритма Кана? O(V + E) по времени и O(V + E) по памяти на список смежности. ### Что вернёт алгоритм Кана на графе с циклом? Неполный порядок. Поэтому в конце сравнивают длину ответа с числом вершин. ### Порядок единственный? Нет. Если в очереди две вершины, любую можно взять первой. Оба порядка верные. ### Почему очередь, а не стек? Со стеком алгоритм тоже верен. Очередь даёт порядок по слоям, который чаще просят. --- title: "Эйлеров путь: когда он существует и как найти его за O(E)" url: https://algopath.pro/ru/blog/eylerov-put language: ru summary: "Эйлеров путь проходит каждое ребро ровно один раз. Условия существования в одной таблице, алгоритм Хирхольцера на Python и задача Reconstruct Itinerary, в которой он прячется." updated: 2026-09-25 --- # Эйлеров путь: когда он существует и как найти его за O(E) **Короткий ответ.** Эйлеров путь это маршрут, который проходит каждое ребро графа ровно один раз. Существует он только при определённом числе вершин с нечётной степенью. Находят его алгоритмом Хирхольцера за O(E). Ниже условия существования, код на Python и задача Reconstruct Itinerary. ## Что такое эйлеров путь и эйлеров цикл? Эйлеров путь проходит каждое ребро ровно один раз. Начало и конец могут быть разными вершинами. Эйлеров цикл это то же самое, но маршрут возвращается в стартовую вершину. Вершины при этом можно посещать сколько угодно раз. Ограничение только на рёбра. Это главное отличие от гамильтонова пути, где каждая вершина ровно один раз. ## Когда эйлеров путь существует? Ответ даёт подсчёт степеней. Степень вершины это число рёбер при ней. | Граф | Эйлеров цикл | Эйлеров путь | | --- | --- | --- | | Неориентированный | Все степени чётные | Ровно 0 или 2 нечётные вершины | | Ориентированный | У каждой вершины вход равен выходу | Одна вершина с выход - вход = 1, одна с вход - выход = 1, остальные равны | | Плюс всегда | Все рёбра в одной компоненте | Все рёбра в одной компоненте | Если нечётных вершин две, путь идёт из одной в другую. Если ноль, старт где угодно. Проверка занимает один проход по рёбрам. Её делают до поиска, а не после. ## Как работает алгоритм Хирхольцера? Идея одна. Идём по неиспользованным рёбрам, пока идётся. Тупики записываем в ответ. - Стартуем в подходящей вершине. Кладём её в стек. - Пока у вершины на вершине стека есть неиспользованное ребро, идём по нему. Новую вершину кладём в стек. - Рёбер нет, значит вершина тупик. Снимаем её со стека и дописываем в ответ. - Стек пуст, ответ готов. Его надо развернуть. Разворот нужен потому, что тупики записываются с конца. Первым в ответ попадает последняя вершина маршрута. ## Как написать эйлеров путь на Python? Ориентированный граф задан списком рёбер. Ищем путь, старт передаём явно. ``` def euler_path(edges, start): graph = {} for a, b in edges: graph.setdefault(a, []).append(b) stack = [start] path = [] while stack: v = stack[-1] if graph.get(v): stack.append(graph[v].pop()) else: path.append(stack.pop()) return path[::-1] ``` Три детали, на которых держится код. - `graph[v].pop()` удаляет ребро при использовании. Второй раз по нему не пройти. - Стек хранит текущий маршрут. Ответ собирается из тупиков. - Сложность O(E): каждое ребро снимается один раз. Для неориентированного графа ребро надо удалять с обеих сторон. Проще хранить рёбра с номерами и помечать использованные. ## Как решить Reconstruct Itinerary через эйлеров путь? Условие. Даны билеты в виде пар «откуда, куда». Все начинаются из JFK. Построй маршрут, который использует каждый билет один раз. При нескольких вариантах бери лексикографически меньший. Каждый билет это ребро. «Использовать каждый один раз» это эйлеров путь. Условие гарантирует, что он существует. Лексикографический порядок даёт одна правка. Списки соседей сортируем по убыванию, тогда `pop()` снимает самый маленький. ``` def find_itinerary(tickets): graph = {} for a, b in sorted(tickets, reverse=True): graph.setdefault(a, []).append(b) return euler_path_from(graph, "JFK") ``` Проследим на билетах JFK→SFO, JFK→ATL, SFO→ATL, ATL→JFK, ATL→SFO. - Из JFK меньший сосед ATL. Из ATL меньший JFK. Из JFK остался SFO. Из SFO в ATL. Из ATL в SFO. - SFO тупик, записываем. ATL, SFO, JFK, ATL, JFK записываются по мере возврата. - Разворот: JFK, ATL, JFK, SFO, ATL, SFO. Жадный обход без стека тут ломается. Он уйдёт в ATL→JFK→SFO→ATL→SFO и потеряет билет. Стек с тупиками это чинит. ## Как узнать эйлеров путь в условии задачи? Слова, которые на него указывают. - «Использовать каждый билет, каждую дорогу, каждое ребро ровно один раз». - «Нарисовать фигуру, не отрывая руки». - «Собрать цепочку из всех костяшек домино». - «Обойти все улицы и вернуться» для почтальона или уборочной машины. - «Соединить слова так, чтобы последняя буква совпадала с первой». Общий признак: ограничение стоит на рёбрах, а не на вершинах. Если «каждый город один раз», это уже другая задача, и она NP-полная. Сигналы, соседи и шаблоны на шести языках на странице [эйлерова пути](/ru/patterns/eulerian-path). ## Как объяснить эйлеров путь на собеседовании? Три фразы до кода. - «Каждое ребро один раз, значит эйлеров путь. Сначала проверю степени.» - «Иду по неиспользованным рёбрам, тупики дописываю в ответ, в конце разворачиваю.» - «Каждое ребро снимается один раз, поэтому O(E).» Потом код. Потом трассировка на пяти рёбрах, как выше. Про разворот сказать отдельно, о нём забывают. ## На чём ломается эйлеров путь? - Не проверены степени. Алгоритм вернёт путь по части рёбер, и он будет выглядеть правильным. - Старт не в нечётной вершине. Путь оборвётся раньше времени. - Забыт разворот ответа. Маршрут выходит задом наперёд. - Неориентированное ребро удалено с одной стороны. Его пройдут дважды. Как узнавать такие задачи по условию, а не по названию, разобрано в [статье про паттерны](/ru/blog/patterny-algoritmov). ## Вопросы и ответы ### Чем эйлеров путь отличается от гамильтонова? Эйлеров проходит все рёбра, гамильтонов все вершины. Первый ищется за O(E), второй NP-полный. ### Какая сложность у алгоритма Хирхольцера? O(V + E) по времени и O(E) по памяти на стек и ответ. ### Можно ли найти эйлеров путь обычным DFS? Обычный DFS с откатами работает, но может быть экспоненциальным. Хирхольцер без откатов, поэтому линейный. ### Что если граф несвязный? Пути нет, если рёбра лежат в двух компонентах. Изолированные вершины без рёбер не мешают. --- title: "Тренажёр алгоритмов: чем он отличается от банка задач" url: https://algopath.pro/ru/blog/trenazher-algoritmov language: ru summary: "Тренажёр задаёт порядок и заставляет вспоминать. Банк задач даёт объём и оставляет тебя одного. Четыре вопроса, по которым тренажёр стоит проверять до оплаты." updated: 2026-10-15 --- # Тренажёр алгоритмов: чем он отличается от банка задач **Короткий ответ.** Тренажёр алгоритмов задаёт порядок тем, заставляет писать алгоритм по памяти и сам возвращает к пройденному через несколько дней. Банк задач вроде LeetCode даёт объём и поиск по темам, а порядок и повторения оставляет тебе. Тренажёр нужен первым, пока паттерны не в голове. Потом нужен объём. ## Что делает банк задач Банк задач даёт много задач и поиск по темам. LeetCode и Codeforces устроены так. Какой банк под какую цель, разобрано в статье [где решать задачи по программированию](/ru/blog/gde-reshat-zadachi-po-programmirovaniyu). Порядок выбираешь ты. Что повторять и когда, решаешь тоже ты. Работает это, когда паттерны уже в голове. До этого момента объём уходит впустую. ## Почему объём сам по себе не работает Решение с подсказкой ощущается как понимание. Ощущение обманывает. Проверить просто. Закрой разобранную вчера задачу и напиши её с нуля. У большинства выходит каркас и не выходят границы. Границы и есть то, что спрашивают. ## Что делает тренажёр Тренажёр отвечает за три вещи, которых в банке задач нет. - Порядок. Один шаг это один алгоритм, и следующий опирается на предыдущий. - Активное вспоминание. Ты пишешь алгоритм сам, а не читаешь готовое. - Возврат. Тема приходит снова через несколько дней, когда её начало забывать. Про второе есть разбор: [почему вспоминание сильнее перечитывания](/ru/blog/aktivnoe-vspominanie). Про третье тоже: [почему повторять на четвёртый день](/ru/blog/intervalnye-povtoreniya). ## Как выглядит вспоминание на практике Возьми алгоритм, который разбирал вчера. Закрой всё и выполни четыре шага. - Назови вслух, что он делает и на каком сигнале ты его узнаёшь. - Напиши его на пустом листе. Без подглядывания, даже на минуту. - Прогони руками на входе из трёх элементов и на пустом входе. - Сверься с разбором и отметь только то, что забыл. Забытое почти всегда одно и то же. Граница цикла, пустой вход, условие остановки. Список забытого и есть твой план на завтра. Он короче, чем любой курс. ## По каким четырём вопросам его проверять Спроси это до того, как платить. - Код пишешь ты или смотришь готовый разбор? - Есть порядок тем или это снова список задач? - Возвращает ли он к пройденному сам? - Тренирует ли распознавание отдельно от решения? Четвёртый вопрос отсеивает почти всё. Назвать нужный [паттерн](/ru/patterns) по условию это отдельный навык. Первый отсеивает видеокурсы. Смотреть чужое решение приятно и почти бесполезно. Как это выглядит на примере видеокурса, разобрано в [отзыве о NeetCode](/ru/algopath-vs-neetcode). Третий отсеивает списки задач. Без возврата к пройденному забывается почти всё за две недели. ## Тренажёр не заменяет объём Тренажёр идёт первым, а не вместо объёма. Сначала паттерны и память рук, потом сотни задач. Собес через две недели и паттерны ты знаешь? Тогда нужен объём, иди в банк задач. ## Где попробовать AlgoPath устроен как тренажёр. 150 шагов в строгом порядке, каждый алгоритм пишешь ты. Первые 33 шага бесплатны, вместе с раннером и тестами, начиная с урока [что такое алгоритм](/ru/steps/what-is-an-algorithm). Карта аккаунта не нужна. Каждый шаг заканчивается системой, где алгоритм реально работает. Рейт-лимитер, LRU-кеш, git bisect. [Тест на уровень](/ru/placement) из 15 вопросов покажет, с какого шага начинать. --- title: "Монотонный стек: как узнать его в задаче и написать за 10 минут" url: https://algopath.pro/ru/blog/monotonnyy-stek language: ru summary: "Монотонный стек находит ближайшего большего или меньшего соседа за один проход. Слова в условии, которые на него указывают, код на Python, отличие от монотонной очереди и четыре типичные ошибки." updated: 2026-09-23 --- # Монотонный стек: как узнать его в задаче и написать за 10 минут **Короткий ответ.** Монотонный стек это обычный стек с одним правилом. Элементы в нём всегда лежат по возрастанию или по убыванию. Его берут, когда каждому элементу нужен ближайший больший или меньший сосед. Один проход по массиву, O(n) времени. Ниже сигналы в условии, код на Python и типичные ошибки. ## Что такое монотонный стек? Это стек с одним дополнительным правилом. Новый элемент кладут не сразу. Сначала со стека снимают всё, что ломает порядок. Пример для убывающего стека. Идём по массиву `[5, 3, 4, 8]`. - Кладём 5. Стек: [5]. - Кладём 3. Оно меньше 5, порядок цел. Стек: [5, 3]. - Приходит 4. Снимаем 3, оно меньше 4. Кладём 4. Стек: [5, 4]. - Приходит 8. Снимаем 4 и 5. Стек: [8]. Каждое снятие это ответ. Четвёрка сняла тройку. Значит, ближайшее большее справа от 3 это 4. ## Как узнать монотонный стек в условии задачи? Слова в условии выдают паттерн раньше, чем ты начнёшь думать. Вот они. - «Ближайший больший элемент справа» или «слева». - «Следующий тёплый день», «через сколько дней». - «Наибольший прямоугольник в гистограмме». - «Сколько элементов подряд не больше текущего». - Любое «для каждого элемента найди первый, который…». Общий признак один. Для каждого элемента нужен один сосед, а лобовое решение даёт O(n²). Монотонный стек сводит это к O(n). Подробнее о сигналах и соседях паттерна на странице [монотонного стека](/ru/patterns/monotonic-stack). Там же шаблоны на шести языках. ## Как написать монотонный стек на Python? Классическая задача: для каждого элемента найти ближайший больший справа. Нет такого, ставим -1. ``` def next_greater(nums): answer = [-1] * len(nums) stack = [] # индексы, значения по убыванию for i, value in enumerate(nums): while stack and nums[stack[-1]] < value: j = stack.pop() answer[j] = value stack.append(i) return answer ``` Три детали, на которых держится код. - В стеке лежат индексы, а не значения. Так ты знаешь, кому записать ответ. - Цикл `while` снимает всё, что меньше нового элемента. - Каждый индекс кладут один раз и снимают один раз. Отсюда O(n). Нужен ближайший меньший? Поменяй знак сравнения. Нужен сосед слева? Ответ записывай в момент, когда кладёшь элемент. ## Чем монотонный стек отличается от монотонной очереди? Их путают чаще всего. Разница в том, откуда уходят элементы. | Признак | Монотонный стек | Монотонная очередь | | --- | --- | --- | | Откуда снимают | Только с вершины | С обоих концов | | Что ищем | Ближайшего соседа | Максимум или минимум в окне | | Слова в условии | «ближайший», «следующий» | «окно размера k», «за последние k» | | Типичная задача | Next Greater Element | Sliding Window Maximum | Если в условии есть окно фиксированной ширины, это [монотонная очередь](/ru/patterns/monotonic-deque). Если сосед, это стек. ## Какие задачи решает монотонный стек? Пять задач, которые встречаются на собеседованиях чаще других. - Next Greater Element. Базовая, её код выше. - Daily Temperatures. То же самое, но ответ это расстояние в днях. - Stock Span. Сколько дней подряд цена не выше сегодняшней. - Largest Rectangle in Histogram. Для каждого столбца ищем меньших соседей с двух сторон. - Trapping Rain Water. Стек хранит стенки, вода считается при снятии. Первые три это одна и та же задача в разной обёртке. Решил одну, решил все. ## Как разобрать задачу Daily Temperatures? Условие. Дан массив температур по дням. Для каждого дня скажи, через сколько дней станет теплее. Если теплее не станет, ответ 0. Сигнал виден сразу. «Через сколько дней» это расстояние до ближайшего большего справа. Возьмём `[73, 74, 75, 71, 69, 72]`. Идём слева направо, в стеке индексы. - День 0, 73 градуса. Стек пуст, кладём 0. - День 1, 74. Снимаем день 0: ответ для него 1 - 0 = 1. Кладём 1. - День 2, 75. Снимаем день 1, ответ 1. Кладём 2. - Дни 3 и 4, 71 и 69. Оба холоднее, просто кладём. - День 5, 72. Снимаем день 4, ответ 1. Снимаем день 3, ответ 2. Кладём 5. В стеке остались дни 2 и 5. Теплее для них не будет, ответ 0. Итог: `[1, 1, 0, 2, 1, 0]`. Двойка у дня 3 потому, что его снял день 5. Такую проверку стоит делать вслух и на собеседовании. Ошибку в индексе видно только на трассировке. ## Как объяснить монотонный стек на собеседовании? Интервьюер ждёт три фразы, и в таком порядке. - «Для каждого элемента нужен ближайший больший справа. В лоб это O(n²).» - «Держу стек индексов с убывающими значениями. Новый элемент снимает всех, кто меньше.» - «Каждый индекс входит и выходит один раз, поэтому O(n).» Потом код. Потом трассировка на маленьком примере. Сложность в конце ещё раз, по памяти. ## Какие ошибки чаще всего? Ошибки на монотонном стеке однотипные. Проверь свой код по списку. - Хранить значения вместо индексов. Ответ некуда записать. - Перепутать строгое и нестрогое сравнение. Равные элементы ломают ответ. - Забыть, что после цикла в стеке что-то осталось. У этих элементов соседа нет. - Идти не с той стороны. Соседа слева ищут при вставке, справа при снятии. ## Как тренировать монотонный стек? Сначала научись узнавать его по условию. Это отдельный навык, и о нём есть [статья про паттерны](/ru/blog/patterny-algoritmov). Потом напиши `next_greater` по памяти три раза с перерывом в день. Потом Daily Temperatures без подсказок. Ошибся, значит вернись к трассировке из первого раздела. Она короткая, её можно держать в голове. ## Вопросы и ответы ### Монотонный стек хранит индексы или значения? Индексы. По индексу можно достать значение, а по значению индекс нет. ### Какая сложность у монотонного стека? O(n) по времени и O(n) по памяти. Каждый элемент входит и выходит один раз. ### Когда стек возрастающий, а когда убывающий? Ищешь большего соседа, держи стек убывающим. Ищешь меньшего, держи возрастающим. ### Можно ли обойтись без стека? Можно, за O(n²) двумя циклами. На собеседовании это засчитают как неоптимальное решение. --- title: "Два указателя и скользящее окно: как их не путать" url: https://algopath.pro/ru/blog/dva-ukazatelya-i-skolzyashchee-okno language: ru summary: "Два указателя и скользящее окно оба идут по массиву двумя индексами. Различает их смысл этих индексов, и ещё три пары устроены так же." updated: 2026-10-15 --- # Два указателя и скользящее окно: как их не путать **Короткий ответ.** Два указателя сходятся с концов отсортированного массива и ищут пару. Скользящее окно двигает оба индекса вперёд и отвечает про отрезок между ними. Чтобы их не путать, после каждой ошибки записывай паттерн, за которым потянулся. Через двадцать задач виден короткий список пар, которые путаешь именно ты. В машинном обучении это называют [матрицей ошибок](https://en.wikipedia.org/wiki/Confusion_matrix). Строки это правда, столбцы это предсказание. Интересное всегда вне диагонали. ## Почему «неверно» это мало Сайт с задачами даёт один бит. Решено или нет. Промахнуться можно четырьмя способами. Не тот паттерн. Тот паттерн с забытым шагом. Верный подход со сломанным краем. Обычная опечатка. Лечатся они по-разному. Свёрнутые в один красный крестик, они не дают выбрать. И человек открывает следующую задачу. Месяцы идут, место остаётся прежним. ## Четыре пары, которые ловят почти всех **Два указателя и [скользящее окно](/ru/patterns/sliding-window-variable).** Оба идут по массиву двумя индексами. Окно держит корректный отрезок и отвечает про него. Два указателя сходятся с концов, и обычно им нужен отсортированный массив. Спроси, что означают твои два индекса, и пара расходится. **Префиксные суммы и окно.** Окну нужна сумма, которая растёт при расширении. Одно отрицательное число это убивает. Приходят [префиксные суммы](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D1%84%D0%B8%D0%BA%D1%81%D0%BD%D0%B0%D1%8F_%D1%81%D1%83%D0%BC%D0%BC%D0%B0). Сигнал это фраза «могут быть отрицательные числа». Её легко проскочить глазами. Поэтому условие [читают дважды](/ru/blog/kak-reshat-algoritmicheskie-zadachi-na-sobesedovanii). **Перебор с возвратом и динамика.** Оба перебирают варианты. [Возврату](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%B8%D1%81%D0%BA_%D1%81_%D0%B2%D0%BE%D0%B7%D0%B2%D1%80%D0%B0%D1%82%D0%BE%D0%BC) нужны все расстановки. Его цена это количество ответов. [Динамике](https://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) нужно одно число. Она выбрасывает всё кроме лучшего. Различение «перечислить или посчитать» решает большинство случаев. **Обход в ширину и Дейкстра.** Одно и то же, пока у рёбер нет весов. Без весов [обход в ширину](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%B8%D1%81%D0%BA_%D0%B2_%D1%88%D0%B8%D1%80%D0%B8%D0%BD%D1%83) уже даёт кратчайший путь. С весами он молча возвращает неправильный ответ. Молча неправильно хуже падения. Поэтому и существует [Дейкстра](https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%94%D0%B5%D0%B9%D0%BA%D1%81%D1%82%D1%80%D1%8B). ## Что записывать Хватит трёх колонок. - Что было на самом деле. - За чем ты потянулся. - Слово в условии, которое должно было тебя остановить. Третья колонка это и есть лечение. Путаница почти всегда это невзвешенный сигнал. То же утверждение есть в статье про [распознавание паттернов](/ru/blog/patterny-algoritmov). К четвёртой записи одно и то же слово всплывёт снова. ## Что делать с парой Не перечитывай оба паттерна. Напиши своими словами одно предложение, которое их разделяет. «Окно корректно, у двух указателей массив отсортирован». «Возврат перечисляет, динамика считает». Потом возьми две задачи, по одной на каждый, и сделай подряд. Для первой пары это бесплатные уроки [два указателя с двух концов](/ru/steps/two-pointers-opposite-ends) и [окно, которое меняет размер](/ru/steps/sliding-window-variable). Держится именно контраст. Разнесённые на неделю, они и слепились. Промежутки нужны одному паттерну, а не паре: [интервальные повторения](/ru/blog/intervalnye-povtoreniya). ## Честная часть Твоя матрица будет личной и совсем не похожей на общий список сложных тем. У меня месяцами рядом стояли кучи и сортировки. Такой пары нет ни в одной программе. В этом и смысл. Ты собираешь факты о себе. Их меньше, чем кажется. ## Где это встроено [AlgoPath](/ru) ведёт эту матрицу за тебя. Тренажёр пишет названный паттерн против верного. Спутанные пары приходят чаще. Первые 33 шага бесплатные. --- title: "Где решать задачи по программированию" url: https://algopath.pro/ru/blog/gde-reshat-zadachi-po-programmirovaniyu language: ru summary: "Площадка зависит от цели: собеседование, олимпиады и изучение языка ведут в три разных места. Сколько задач нужно на самом деле и чего делать не стоит." updated: 2026-10-15 --- # Где решать задачи по программированию **Короткий ответ.** Площадка зависит от цели. Для собеседования подходят LeetCode и HackerRank, для олимпиад Codeforces, для изучения языка Codewars и Exercism. Задач нужно меньше, чем принято считать: одна задача, разобранная до конца, стоит пяти, решённых по подсказке. Выбери одну площадку и держись её месяц. ## Готовишься к собеседованию Тебе нужен формат интервью, а не спортивное программирование. Задачи там короткие, а оценивают подход и код. LeetCode даёт объём и разбивку по компаниям. HackerRank даёт тот самый отборочный тест, которым многие компании отсеивают. Условия на LeetCode только на английском, и что с этим делать, разобрано в статье [LeetCode на русском](/ru/blog/leetcode-na-russkom). Сравнение площадок под эту цель лежит в [разборе семи аналогов LeetCode](/ru/leetcode-alternatives). ## Идёшь в олимпиады и контесты Здесь другая игра. Ценятся скорость и умение выжать нужную сложность из жёстких ограничений. Codeforces это основная площадка с регулярными контестами. Задачи там в среднем тяжелее собеседовательных. Начинать с олимпиадных задач ради собеседования не стоит. Навык пересекается лишь частично. ## Учишь язык Нужны упражнения, а не алгоритмы. Codewars даёт короткие ката, Exercism даёт разбор от живого ментора. Алгоритмы на этом этапе мешают. Сначала синтаксис доходит до автоматизма, потом приходят структуры данных. Признак, что пора дальше: пишешь цикл и не думаешь о нём. ## Сколько задач нужно Меньше, чем принято считать. Счёт решённых задач плохо предсказывает результат интервью. Одна задача, разобранная до конца, стоит пяти прорешанных по подсказке. До конца значит четыре шага. Назвал паттерн. Написал сам. Объяснил вслух. Вернулся к ней через неделю. Про четвёртый шаг есть отдельный разбор: [почему повторять на четвёртый день](/ru/blog/intervalnye-povtoreniya). ## Чего не делать - Решать подряд по списку. Список не знает, что у тебя не получается. - Открывать решение через десять минут. Так тренируется чтение, а не решение. - Менять площадку каждый месяц. Прогресс теряется вместе с историей. ## Признаки, что площадка не та Заметны они быстро, за пару недель. - Задачи интересные, но на собеседовании таких не спрашивают. Это олимпиадный уклон. - Решаешь легко и подряд. Уровень ниже твоего, и время уходит в ноль. - Без подсказки не сдвигаешься совсем. Уровень выше, нужен шаг назад. Средний случай самый вредный. Задачи посильные, прогресса нет, и понять это можно только на интервью. ## Как выбрать за минуту Проведи один мок-собес и посмотри, где сыпется. - Читаешь условие и зависаешь. Это распознавание, гоняй [паттерны](/ru/patterns). - Подход называешь верно, а код выходит кривой. Это память рук, пиши алгоритмы сам на [тренажёре алгоритмов](/ru/blog/trenazher-algoritmov). - И то и другое нормально, но медленно. Это объём, решай задачи пачками. Чаще всего ломается первое. [Распознавание паттернов](/ru/blog/patterny-algoritmov) тренируют реже всего, а спрашивают первым. ## С чего начать на этой неделе Выбери одну площадку и держись её месяц. Смена площадки обнуляет историю и ощущение прогресса. Возьми пять задач на одну тему, а не пять тем по задаче. Паттерн виден только на повторе. Заведи список того, что забыл. Он короткий и он честнее любого счётчика решённых задач. [Тест на уровень](/ru/placement) из 15 вопросов не требует аккаунта. Он подскажет тему, с которой начинать. --- title: "Алгоритмическая секция: что на ней оценивают" url: https://algopath.pro/ru/blog/algoritmicheskaya-sektsiya language: ru summary: "Интервьюер оценивает то, что ты произносишь, а не то, что думаешь. Четыре вещи, которые надо говорить вслух, и как их отрепетировать." updated: 2026-10-15 --- # Алгоритмическая секция: что на ней оценивают **Короткий ответ.** На алгоритмической секции оценивают четыре вещи: понял ли ты задачу, выбрал ли подход осознанно, пишешь ли верный код и проверяешь ли себя. Три из четырёх живут у тебя в голове, поэтому их надо проговаривать. Рабочий код после десяти минут молчания получит меньше, чем половина решения вслух. ## Что он на самом деле заполняет Почти любая шкала сводится к четырём вопросам. Понял ли ты задачу. Выбрал ли подход осознанно. Умеешь ли писать верный код. Проверил ли себя. Три из четырёх живут у тебя в голове. Не произнёс, значит их нет. Кто не может проследить рассуждение, считает, что его не было. ## Четыре фразы Говори это примерно в таком порядке, каждый раз. **«Значит на входе X, вернуть надо Y».** Подтверждает задачу. Страховка от двадцати потраченных минут. **«Цепляет слово Z, оно намекает на окно».** Эту фразу и пропускают. Оценивают её строже всего. Она показывает распознавание, а не заученность. **«Наивный вариант это O(n²). Сначала скажу почему, потом улучшу».** Назвать медленный вариант первым это сила. Видно, что ты понимаешь, что именно меняешь. Если нотация подзабылась, начни со статьи [Big O простыми словами](/ru/blog/big-o-prostymi-slovami). **«Проверю на пустом массиве».** Проверка себя без напоминания поднимает на целую ступень. ## Провал, о котором не предупреждают Люди озвучивают набор текста. «Сейчас заведу переменную left, поставлю ноль, теперь цикл while». Озвучивание набора текста повторяет то, что делает скринридер. Интервьюер это и так видит. Озвучивай решения. Почему два указателя, а не словарь. Почему left двигается сюда, а не туда. Что обязано оставаться верным на каждой итерации. У решения есть альтернатива. Нет альтернативы, значит говорить не стоило. ## Что говорить, когда застрял Скажи, что застрял, и скажи, что у тебя есть. «Простое окно не подходит из-за отрицательных. Выбираю между сортировкой и префиксными суммами. Сортировка теряет порядок, а порядок тут, кажется, важен». Три предложения, две работы. Тебя оценивают, пока ты думаешь. Интервьюеру есть за что зацепиться. Молчание не даёт ему ничего. Поэтому он ждёт. Дальше всё делают часы. ## Как это тренировать Говорить и решать одновременно это отдельный навык. Сначала он отнимает внимание у решения. Исследователи не зря десятилетиями используют [протокол размышления вслух](https://en.wikipedia.org/wiki/Think_aloud_protocol). Поэтому репетируй отдельно. - Одна задача в неделю вслух, на запись, в одиночестве. Ставь её в [слот под таймером](/ru/blog/volnenie-na-sobesedovanii). - Прослушай на двойной скорости. Посчитай, сколько предложений были решениями. - Объясни решённую задачу человеку, который не программирует. Это [метод утёнка](https://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D1%82%D0%BE%D0%B4_%D1%83%D1%82%D1%91%D0%BD%D0%BA%D0%B0), направленный на собственное понимание. Слушать себя неприятно. И это самая быстрая обратная связь из доступных. ## Где это встроено [AlgoPath](/ru) просит обоснование письменно. Назови паттерн. Скажи, какой сигнал на него указал. Потом код. Пишешь это каждый раз, и произнести вслух становится коротким шагом. Где стоит попытка вслух: [как готовиться к алгоритмам](/ru/blog/kak-podgotovitsya-k-algoritmicheskomu-sobesedovaniyu). Что говорить первым, следует из [чтения условия](/ru/blog/kak-reshat-algoritmicheskie-zadachi-na-sobesedovanii). Первые 33 шага бесплатные. --- title: "LeetCode на русском: чего нет и что с этим делать" url: https://algopath.pro/ru/blog/leetcode-na-russkom language: ru summary: "Русской версии LeetCode нет: интерфейс, условия задач и обсуждения английские. Сорок слов, которые закрывают почти любое условие, и что есть на русском целиком." updated: 2026-10-15 --- # LeetCode на русском: чего нет и что с этим делать **Короткий ответ.** Русской версии LeetCode нет: интерфейс, условия и обсуждения там на английском, переключателя языка нет. Автоперевод стирает точные слова вроде subarray и at most, на которых держится задача. Помогают две вещи. Выучить около сорока слов из условий за один вечер. Брать теорию на русском, а решать на LeetCode. ## Почему автоперевод не спасает Условие задачи держится на нескольких точных словах. Убери точность, и задача станет другой. Возьми фразу «longest subarray with sum at most k». Работают тут четыре слова. - Subarray это подряд идущий кусок. Не любой набор элементов. - Longest значит, что ответ это число. Не сам кусок. - At most читается как «не больше». Не «ровно». - Слова positive нет. Значит числа могут быть отрицательными. Переводчик передаст общий смысл и сотрёт эти границы. Ты решишь соседнюю задачу и не поймёшь, почему тесты красные. ## Сорок слов закрывают почти всё Словарь условий маленький и всё время повторяется. Существительные: array, subarray, substring, index, node, tree, graph, edge. Свойства: sorted, distinct, unique, non-empty, non-negative, in place. Ограничения: at most, at least, exactly, contains, may contain, guaranteed. Действия: return, find, count, remove, reverse, merge, rotate. Выучить их один раз дешевле, чем переводить каждую задачу. Уходит на это вечер. ## На собеседовании английский тоже нужен Условие на техническом интервью часто дают по-английски. Даже в русскоязычной компании. Причина простая. Задачи берут из тех же банков, и переводить их никто не станет. Разбирать новый алгоритм проще на родном языке. Перенести разобранное на английскую формулировку легче, чем идти обратно. ## Что действительно есть на русском Полностью русскоязычных тренажёров по алгоритмам мало. [Сравнение аналогов LeetCode](/ru/leetcode-alternatives) показывает, кто что даёт и сколько стоит. Площадки под другие цели, от олимпиад до изучения языка, собраны в статье [где решать задачи по программированию](/ru/blog/gde-reshat-zadachi-po-programmirovaniyu). AlgoPath на русском целиком: сайт, все 150 шагов и подсказки. Код при этом пишется на одном из шести языков. Обсуждения под задачами заменить труднее всего. Это сильная сторона LeetCode, и её стоит признать честно. ## Как читать обсуждения Обсуждения под задачей полезнее самого решения. Там пишут, где человек ошибся и почему. Читать их целиком не надо. Достаточно верхнего комментария и одного с пометкой о крайнем случае. Язык там простой и повторяющийся. Пара фраз закрывает почти любой разбор. - Edge case значит крайний случай. Пустой вход, один элемент, все одинаковые. - Off by one значит ошибка на единицу. Обычно в границе цикла. - TLE значит слишком медленно. Сложность не та, а не код кривой. - Brute force значит решение в лоб. С него начинают, им не заканчивают. ## Как совместить два языка Держи слои раздельно. - Теорию и разбор бери на русском. Думать так быстрее. - Решай там, где задач больше. Английское условие читай медленно. - Незнакомое слово выписывай. За месяц список закончится. Читать условие это отдельный навык. Про него есть [разбор одного условия по словам](/ru/blog/kak-reshat-algoritmicheskie-zadachi-na-sobesedovanii). [Тест на уровень](/ru/placement) из 15 вопросов не требует аккаунта. Он скажет, с какой темы начинать. --- title: "Волнение на собеседовании: как решать под таймером" url: https://algopath.pro/ru/blog/volnenie-na-sobesedovanii language: ru summary: "Таймер это диагностика, а не уровень сложности. Когда его включать и что говорить себе в момент, когда волнение накрывает." updated: 2026-10-15 --- # Волнение на собеседовании: как решать под таймером **Короткий ответ.** Решай без таймера, пока не выдаёшь паттерн по памяти, и только потом включай таймер. Раньше он тренирует только панику. Тревога занимает рабочую память, поэтому состояние надо вынести наружу. Проговори вход и выход, реши руками пример на четыре элемента и посмотри, что делала рука. ## Что таймер на самом деле забирает Давление не замедляет набор текста. Оно забирает рабочую память. В [рабочей памяти](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B1%D0%BE%D1%87%D0%B0%D1%8F_%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D1%8C) ты держишь инвариант, границы цикла и краевые случаи. Тревога занимает её часть. Эшкрафт и Кирк показали это на математике в 2001 году. Тот же механизм, когда забываешь подход, который знал десять секунд назад. Таймер забирает ровно то, на что ты опирался. Держать несколько вещей в голове разом. Всё записанное остаётся. Вот на это и надо давить. ## Три фазы по порядку **Фаза 1. Без таймера.** Решай, пока не сможешь выдать паттерн верно с закрытой вкладкой. Таймер здесь скажет только то, что ты и так знаешь. **Фаза 2. Щедрый таймер.** 40 минут на задачу, которую обычно делаешь за 20. Часы появляются, угрозы нет. **Фаза 3. Как на собеседовании.** 25 до 30 минут, незнакомая задача, вслух, без пауз. Что говорить вслух, разобрано в статье [алгоритмическая секция](/ru/blog/algoritmicheskaya-sektsiya). Немного напряжения помогает, много перестаёт. У этой кривой есть имя: [закон Йеркса-Додсона](https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BA%D0%BE%D0%BD_%D0%99%D0%B5%D1%80%D0%BA%D1%81%D0%B0_%E2%80%94_%D0%94%D0%BE%D0%B4%D1%81%D0%BE%D0%BD%D0%B0). Большинство прыгает в третью фазу на первой неделе. Потом решает, что не тянет алгоритмы. Честный вывод другой. Проверили раньше, чем научили. ## Скрипт спасения на пять минут Голова опустела. Вот скрипт. Он работает потому, что не требует рабочей памяти вообще. - Вслух проговори вход и выход. - Напиши руками маленький пример на четыре элемента. - Реши этот пример на бумаге медленно. Любым способом. - Посмотри, что твоя рука делала на каждом шаге. Обычно это и есть алгоритм. - Скажи, на какой паттерн похоже. И какое слово на него указывает. Это [чтение условия](/ru/blog/kak-reshat-algoritmicheskie-zadachi-na-sobesedovanii) под давлением. Каждая строка снаружи головы. Бумага, голос, написанный пример. Ничего держать в уме не надо. Тренируй сам скрипт, а не только задачи. Понадобится он один раз. От него зависит, будут плохими двадцать минут или весь час. ## «Не успел» это три разных диагноза Выйти за время можно по трём причинам, и лечатся они по-разному. Долго выбирал паттерн значит проблема с [распознаванием](/ru/blog/patterny-algoritmov). Долго писал значит эту структуру ты писал слишком мало раз. Долго отлаживал значит краевые случаи не разобрал в начале. Тренируйся на том языке, на котором пишешь: [их теперь шесть](/ru/blog/tri-novyh-yazyka-typescript-java-csharp). Записывай, что именно было. Иначе «не успел» становится всем диагнозом. А из этой фразы делать нечего. ## Одно честное применение таймера Таймер отвечает на один вопрос. Можешь ли ты пройти задачу без паузы, в которой ты что-то смотришь. Знание об этом полезно за две недели до собеседования. За два месяца оно почти бесполезно. ## Где это встроено В [AlgoPath](/ru) работа под временем отделена намеренно. Шаги идут без таймера. Тренажёр распознавания и гонтлет со временем. Оба после того, как паттерн уже стоит. Первые 33 шага бесплатные. --- title: "Интервальные повторения: почему повторять на 4-й день, а не завтра" url: https://algopath.pro/ru/blog/intervalnye-povtoreniya language: ru summary: "Слишком раннее повторение стоит времени и не даёт ничего. Где должен быть интервал и что интервальные повторения делают с памятью." updated: 2026-10-15 --- # Интервальные повторения: почему повторять на 4-й день, а не завтра **Короткий ответ.** Новый паттерн повторяй на 4-й день, потом на 11-й и на 25-й. Повторение на следующий день слишком раннее: память ещё на месте, и восстанавливать нечего. Лучший промежуток составляет от 10 до 20 процентов срока, на который нужна память. До собеседования 3 месяца? Значит, от 9 до 18 дней. ## Кривая под этим Эббингауз измерял собственную память в 1885 году. Он построил [кривую забывания](https://ru.wikipedia.org/wiki/%D0%9A%D1%80%D0%B8%D0%B2%D0%B0%D1%8F_%D0%B7%D0%B0%D0%B1%D1%8B%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F). Кривая забывания падает резко в начале, а потом идёт полого. Каждое удачное повторение сбрасывает падение и делает следующее более пологим. Остаётся один вопрос: когда. Ответ позже, чем кажется безопасным. ## Откуда числа Сепеда с коллегами провели самый крупный такой опыт в 2008 году. Исследователи меняли промежуток между изучением и повторением. Тестировали сильно позже. Вся эта область называется [интервальные повторения](https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%82%D0%B5%D1%80%D0%B2%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D0%BF%D0%BE%D0%B2%D1%82%D0%BE%D1%80%D0%B5%D0%BD%D0%B8%D1%8F). Лучший промежуток зависел от того, насколько долго память должна прожить. Примерно 10 до 20 процентов от этого срока. Собеседование через 3 месяца требует 9 до 18 дней между повторениями одного паттерна. Через 3 недели промежуток составляет от 2 до 4 дней. Повторять бинарный поиск каждый вечер целый месяц значит потратить месяц впустую. ## Лестница 1, 3, 7, 16, 35 дней. Вспомнил паттерн верно, он поднимается на ступень. Не вспомнил, падает на первый день. Этот метод называется [система Лейтнера](https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D0%9B%D0%B5%D0%B9%D1%82%D0%BD%D0%B5%D1%80%D0%B0). Важно в ней одно. Слабые паттерны возвращаются постоянно. Сильные уходят с дороги. Первая ступень короткая намеренно. Паттерн, выученный час назад, хрупкий. Одно вспоминание на следующий день делает длинные интервалы выносимыми. ## Ошибка, которая всё это прячет Подготовку обычно строят по темам. Неделя на деревья. Неделя на графы. Неделя на динамику. Все повторения внутри недели идут по горячему материалу. Значит все они лёгкие, а лёгкие значит дешёвые. В конце у тебя восемь пройденных тем и уверенно работает последняя. Лечится перемешиванием. В одном занятии разные паттерны. Ощущается заметно хуже. Сначала надо понять, какой это паттерн. Именно этот шаг и проверяет собеседование. ## Что делать на этой неделе Держи один список паттернов с датой рядом с каждым. Каждый день бери те, что подошли. По каждому закрой всё и восстанови ядро по памяти. Верно, отодвигай дату. Неверно, назад на завтра. Десять минут в день держат гораздо больше, чем трёхчасовое воскресенье. Внутри этих десяти минут должно быть [активное вспоминание](/ru/blog/aktivnoe-vspominanie), а не перечитывание. Где расписание стоит в неделе: [как готовиться к алгоритмам](/ru/blog/kak-podgotovitsya-k-algoritmicheskomu-sobesedovaniyu). ## Где это работает [AlgoPath](/ru) считает расписание за тебя. Попытки в тренажёре кормят планировщик Лейтнера. Спутанные паттерны приходят снова. Крепкие отступают. Список ведёшь не ты. Его ведёт трек. Первые 33 шага бесплатные. --- title: "Активное вспоминание: почему оно сильнее перечитывания" url: https://algopath.pro/ru/blog/aktivnoe-vspominanie language: ru summary: "Перечитывать решение это самый слабый способ выучить алгоритм. Активное вспоминание восстанавливает его с пустого листа, и это самый сильный." updated: 2026-10-15 --- # Активное вспоминание: почему оно сильнее перечитывания **Короткий ответ.** Активное вспоминание значит закрыть решение и восстановить алгоритм с пустого листа. Перечитывание идёт гладко, и мозг принимает эту гладкость за знание. Собеседование проверяет вспоминание. В опыте Рёдигера и Карпике 2006 года через неделю с большим отрывом выиграли те, кто вспоминал. Прочитал один раз, закрыл, восстановил, сравнил. ## Почему перечитывание кажется правильным Прочитай объяснение Дейкстры два раза. Второй проход идёт гладко. Ничего не удивляет. Мозг принимает эту гладкость за знание. На самом деле построилось узнавание. А собеседование просит вспоминание. Расходятся они в худший момент: пустой редактор и человек, который смотрит. ## Что показали исследования У эффекта есть название: [эффект тестирования](https://ru.wikipedia.org/wiki/%D0%AD%D1%84%D1%84%D0%B5%D0%BA%D1%82_%D1%82%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F). За ним большая литература. Рёдигер и Карпике поставили классический опыт в 2006 году. Студенты читали текст, потом либо перечитывали, либо проходили тест на вспоминание. Через пять минут выигрывали те, кто перечитывал. Через неделю с большим отрывом выигрывали те, кто вспоминал. Карпике и Блант повторили в 2011 году, уже против составления схем. Составление схем даёт более активную учёбу. Вспоминание всё равно выиграло. Направление устойчивое. Произвести ответ полезнее, чем его просмотреть. ## Как выглядит вспоминание для алгоритма Закрой все вкладки. Дальше по порядку: - Вслух скажи тремя предложениями, что делает алгоритм. - Напиши по памяти основной цикл. Без заметок и без автодополнения. - Назови одну реальную систему, где он работает. - Назови паттерн, с которым его путают, и разницу. Открой заметки и отметь, что не вспомнил. Отметка и есть всё упражнение. Не вспомнить, а потом увидеть ответ полезнее, чем прочитать его дважды. ## Будет неприятнее Вспоминать некомфортно. Сидишь перед пустым листом, и ничего не приходит. Роберт Бьорк называет это [желательной трудностью](https://en.wikipedia.org/wiki/Desirable_difficulty). Усилие при извлечении и делает память прочной. Поэтому занятие, после которого осталось тяжёлое чувство, обычно и сработало. А гладкое дало меньше всего. Из-за этой перевёрнутой обратной связи почти никто и не переключается. ## Одно правило Никогда не читай решение дважды. Прочитал один раз. Закрыл. Восстановил. Сравнил. Вспоминание работает в паре с [интервальными повторениями](https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%82%D0%B5%D1%80%D0%B2%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D0%BF%D0%BE%D0%B2%D1%82%D0%BE%D1%80%D0%B5%D0%BD%D0%B8%D1%8F): промежуток важен не меньше самой попытки. Интервальные повторения требуют посчитать всего два числа. Сколько задач ты прочитал за месяц. И сколько написал бы сегодня с закрытой вкладкой. Второе число обычно шокирует. И меряют на собеседовании именно его. ## Где это встроено В [AlgoPath](/ru) вспоминание идёт по умолчанию. Каждый шаг заканчивается проверкой. Объясни без заметок. Перепиши ядро с нуля. Скажи, почему оно подходит к реальному кейсу. Закрыть шаг чтением нельзя. Из доказанных шагов и собираются [уровни открытого рейтинга](/ru/blog/urovni-i-otkrytyy-reyting). Где вспоминание стоит в неделе подготовки, разобрано в статье [как готовиться правильно](/ru/blog/kak-podgotovitsya-k-algoritmicheskomu-sobesedovaniyu). Первым вспоминаешь паттерн, а это [отдельный навык](/ru/blog/patterny-algoritmov). Первые 33 шага бесплатные. --- title: "Как решать алгоритмические задачи на собеседовании: сначала условие" url: https://algopath.pro/ru/blog/kak-reshat-algoritmicheskie-zadachi-na-sobesedovanii language: ru summary: "Прочитай условие дважды, прежде чем что-то писать. Одно условие, разобранное по словам, и что каждое слово отсекает." updated: 2026-10-15 --- # Как решать алгоритмические задачи на собеседовании: сначала условие **Короткий ответ.** Прочитай условие дважды, прежде чем писать код. Первый раз ищешь, что спрашивают. Второй раз ищешь ограничения: подмассив или подпоследовательность, «не больше» или «ровно», есть ли отрицательные числа и каков размер n. Эти несколько слов и выбирают алгоритм. Запиши их в шесть строк, потом пиши код. Разберём одно условие. ## Условие > Дан массив целых чисел и число k. Верни длину самого длинного подмассива, сумма которого не больше k. В массиве могут быть отрицательные числа. 1 ≤ n ≤ 200000. Двадцать восемь слов. Шесть из них решают, какой будет алгоритм. ## «подмассив» Подмассив идёт подряд. [Подпоследовательность](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D1%8C) нет. Одно слово, и половина вариантов отпала. Подряд означает [окно](/ru/patterns/sliding-window-variable) или [префиксные суммы](/ru/patterns/prefix-sums). Не подряд означает сортировку или динамику. Слово «подряд» читают неправильно чаще всего. Оно самое дорогое на странице. ## «длину» Ты возвращаешь число. Значит сам подмассив хранить не нужно. Нужен максимум. Одна переменная с лучшим значением. Одно сравнение. Вся бухгалтерия. ## «не больше k» Не больше, а не ровно. Ровно k тянуло бы к словарю префиксных сумм. Не больше k это диапазон. Тут помогает порядок. Словарь бесполезен. Ровно k это как раз случай для [префиксных сумм](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D1%84%D0%B8%D0%BA%D1%81%D0%BD%D0%B0%D1%8F_%D1%81%D1%83%D0%BC%D0%BC%D0%B0) в словаре. Разница в одном слове. Структуры данных разные. ## «могут быть отрицательные» Вот ловушка. Если чисел меньше нуля нет, расширение окна всегда увеличивает сумму. Именно это делает два указателя законными. Отрицательные ломают правило. Сумма перестаёт двигаться в одну сторону. Значит сужение окна может её поднять. Два указателя стали неверными. Верны префиксные суммы плюс упорядоченная структура. Одна фраза превратила решение на 5 строк в решение на 20. Фраза всё это время была в условии. ## «1 ≤ n ≤ 200000» 200 тысяч отсекают всё квадратичное. Это 40 миллиардов операций. Справочник по тому, что разрешает каждая граница: [временная сложность](https://ru.wikipedia.org/wiki/%D0%92%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D0%B0%D1%8F_%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C_%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC%D0%B0). n log n проходит спокойно. Сортировка по карману. Бинарный поиск внутри цикла тоже. Граница на n прямо говорит, какую сложность ждёт интервьюер. Читай её как инструкцию. Что разрешает каждая граница, собрано в [таблице сложности алгоритмов](/ru/blog/tablitsa-slozhnosti-algoritmov). ## Шесть строк до кода - Что на входе и что на выходе. - Размер входа. - Подряд или нет. - Отсортировано или нет. - Края: пусто, один элемент, все отрицательные. - Какой это паттерн. И какое слово тебя к нему привело. Последняя строка главная. Слова, которые указывают на каждый паттерн, собраны в [шпаргалке по паттернам алгоритмов](/ru/patterns/cheat-sheet). Не можешь назвать слово, значит угадал. Угаданный ответ не учит ничему. ## Две минуты, а не тридцать секунд В интервью на 45 минут две минуты на чтение кажутся роскошью. Если от часов пустеет голова, есть [скрипт спасения на пять минут](/ru/blog/volnenie-na-sobesedovanii). Две минуты на чтение стоят дешевле всего в интервью. Неправильно прочитанное условие стоит всей сессии. Узнаешь ты на тридцатой минуте. На доске уже чужой ответ. Быстрое чтение никто не оценивает. Остальная рутина вокруг этого в статье [как готовиться правильно](/ru/blog/kak-podgotovitsya-k-algoritmicheskomu-sobesedovaniyu). ## Где это тренируется В [AlgoPath](/ru) каждый шаг начинается с сигналов. Это слова в задаче, которые указывают на паттерн. [Назвать паттерн](/ru/blog/patterny-algoritmov) надо до кода. В тренажёре распознавания задача приходит без метки. Сначала называешь паттерн и обосновываешь. Первые 33 шага бесплатные. --- title: "Уровни и рейтинг, который видно всем" url: https://algopath.pro/ru/blog/urovni-i-otkrytyy-reyting language: ru summary: "AlgoPath давно считает очки. Доказал шаг, получил очки, поднялся в таблице." updated: 2026-10-15 --- # Уровни и рейтинг, который видно всем AlgoPath давно считает очки. Доказал шаг, получил очки, поднялся в таблице. С очками одна беда. Они тебя оценивают. Они не говорят, что делать дальше. «Набери ещё 40 очков» это не задача. По ней невозможно действовать. Поэтому в AlgoPath появились уровни. Уровень собирается из тем. ## Восемь ступеней Уровней всего восемь: Новичок, Ученик, Практик, Инженер, Специалист, Эксперт, Мастер, Архитектор. Ступень берётся закрытыми темами. Не набранными очками. Ученику нужны Основы. Практику нужны Big-O, массивы и хеш-таблицы. Каждая ступень перечисляет всё, что ей нужно. Тема закрыта, когда доказан каждый её шаг. Своим кодом. Своими тестами. Половина темы не даёт ничего. Это сделано нарочно. Собрать лёгкие шаги из пяти тем значит оставить пять дыр. Закрыть одну тему значит не оставить ни одной. Взятая ступень остаётся твоей навсегда. Требования потом могут стать строже. Вниз никто не поедет. У каждой ступени своя страница. На ней написано, каких тем она хочет и сколько это шагов. [Лестницу из восьми ступеней](/ru/levels) может смотреть кто угодно. ## Цифра, от которой есть польза Дашборд теперь говорит, сколько осталось. «Следующий уровень: Инженер. Осталось шагов: 13. Два указателя и окна: 1 из 5.» Эту задачу можно открыть завтра и сделать часть. Старая строка говорила «#7 · 42 очка». Она оценивала. Она никуда не вела. ## Рейтинг теперь открыт Раньше таблица была за входом. Соревнование видели только те, кто уже в нём. Странный способ проводить состязание. Таблица теперь открыта. Смотреть может кто угодно. Имена, аватары, очки, пройденные шаги, уровень. Поисковикам вход туда закрыт. Посетитель прочитает. Google не проиндексирует. Галочка в профиле убирает тебя из таблицы и мест. Она закрывает и карточку участника. Одна галочка, один понятный эффект. На собственный прогресс это не влияет никак. ## Письмо, когда поднимаешься Взял ступень, пришло короткое письмо. В нём тема, которую ты закрыл. В нём следующая ступень. И сколько до неё осталось. Одно письмо на уровень. Не на каждый шаг. Письмо можно выключить в профиле. Галочка стоит рядом с рассылкой. ## Про бесплатные шаги Первые 33 шага бесплатны. Темы после них платные. Инженеру нужно 33 шага. Это ровно бесплатная часть, шаг в шаг. Значит, бесплатно доходишь до Инженера и там останавливаешься. Лестница показывает, где кончается бесплатная часть. Это честнее, чем платная стена без предупреждения. Почему трек выстроен так, написано в статье [зачем я сделал AlgoPath](/ru/blog/zachem-ya-sdelal-algopath). ## Что не поменялось Очки по-прежнему определяют места. Стрик по-прежнему считает дни. В том, как доказывается шаг, не изменилось ничего. Уровни легли поверх уже сделанной работы. Каждый доказанный тобой шаг посчитан. Открой дашборд, твоя ступень уже там. Неделя занятий под эти ступени: [как готовиться к алгоритмам](/ru/blog/kak-podgotovitsya-k-algoritmicheskomu-sobesedovaniyu). Иди посмотри, на какой ты. --- title: "Паттерны алгоритмов: навык, который никто не тренирует" url: https://algopath.pro/ru/blog/patterny-algoritmov language: ru summary: "Ты умеешь писать бинарный поиск. Понять, что задача его просит, это отдельный навык, и его почти никто не тренирует." updated: 2026-10-15 --- # Паттерны алгоритмов: навык, который никто не тренирует **Короткий ответ.** Паттерны алгоритмов это около 50 повторяющихся типов задач, у каждого есть пара слов-примет. Отсортированный вход и пара ведут к двум указателям, подряд идущий отрезок ведёт к скользящему окну. [Бинарный поиск](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA) напишет большинство разработчиков, а узнать его в задаче без подписи сможет меньшинство. Каждый туториал объявляет ответ в заголовке. Почему туториалы так устроены, разобрано в статье [почему алгоритмы ненавидят](/ru/blog/nenavizhu-algoritmy). ## Что такое распознавание Опытный человек читает условие. Через секунд десять что-то срабатывает. Срабатывает короткий список. «Пахнет окном». «Это граф под маской». «Ответ монотонный, значит можно искать по нему». Такое узнавание строится на выученном соответствии. От поверхностных признаков задачи к небольшому набору кандидатов. У шахматистов то же самое для позиций на доске. Психологи называют это [чанкингом](https://en.wikipedia.org/wiki/Chunking_(psychology)). Чанкинг берётся из насмотренности. Насмотренность можно устроить намеренно. Обычно её получают по 30 секунд за раз, как прелюдию к часу написания кода. Тренируйте отдельно, и пропорция меняется. ## Сигналов немного, и они выучиваются Большинство задач с собеседований идёт примерно из 50 паттернов. У каждого есть пара примет. **Два указателя с концов.** Вход отсортирован. Ответ это пара. **Скользящее окно.** Подряд идущие, подстрока, подмассив. Плюс самый длинный, самый короткий или счёт под лимитом. **Бинарный поиск по ответу.** Минимальное или максимальное значение, при котором ещё работает. Проверить одно значение легко. **[Куча](https://ru.wikipedia.org/wiki/%D0%9A%D1%83%D1%87%D0%B0_(%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85)).** k лучших из чего-то. Или наименьший элемент, снова и снова. **[Монотонный стек](/ru/patterns/monotonic-stack).** Для каждого элемента: следующий больший или меньший. **[Система непересекающихся множеств](https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D0%BD%D0%B5%D0%BF%D0%B5%D1%80%D0%B5%D1%81%D0%B5%D0%BA%D0%B0%D1%8E%D1%89%D0%B8%D1%85%D1%81%D1%8F_%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D1%81%D1%82%D0%B2).** Объекты сливаются в группы. Вас всё время спрашивают, в одной ли группе двое. Шесть паттернов. По одной строке. Это помещается в голову сегодня. Сигналы всех 52 собраны в одну таблицу, [шпаргалку по паттернам алгоритмов](/ru/patterns/cheat-sheet). Работа в том, чтобы они срабатывали сами. ## Учите соседей, а не только сигналы Сигналы это половина дела. Вторая половина это паттерн, который стоит рядом и выглядит почти подходящим. **Два указателя против скользящего окна.** Оба ведут два индекса. Указатели сходятся с концов и требуют сортировки. Окно двигает оба индекса в одну сторону. Как вести список своих спутанных пар, разобрано в статье [два указателя и скользящее окно](/ru/blog/dva-ukazatelya-i-skolzyashchee-okno). **[Жадность](https://ru.wikipedia.org/wiki/%D0%96%D0%B0%D0%B4%D0%BD%D1%8B%D0%B9_%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC) против [динамического программирования](https://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5).** Оба строят ответ по шагам. Жадность безопасна, только если локальный выбор невозможно потом пожалеть. **BFS против DFS.** Оба обходят все вершины. У кратчайшего пути в невзвешенном графе верный ответ один. **Сортировка против кучи.** Оба дают порядок. Для топ-3 из миллиона сортировка всего миллиона не подходит. Распознавание ломается здесь. На двух, которые похожи. ## Как это тренировать **Спрячьте категорию.** Перемешайте список, закройте теги или возьмите источник, сделанный для этого. **Отвечайте до решения.** Паттерн и слова, которые вам его выдали. 3 минуты. **Дальше остановитесь.** Угадали? Идите дальше. Решение съедает время, которое нужно на повторы. **Записывайте каждый промах парой.** Что было. Что вы сказали. **Подмешивайте старое.** Распознавание на теме этой недели только напоминает, какая сейчас неделя. Двадцать задач в час это норма. Двести это несколько недель лёгкой практики. Примерно там и происходит сдвиг. Где это стоит в рабочей неделе, разобрано в статье [как готовиться правильно](/ru/blog/kak-podgotovitsya-k-algoritmicheskomu-sobesedovaniyu). ## Как это ощущается, когда заработало Вы читаете задачу, и первая мысль это категория. Всю работу по решению всё равно делать вам. Но вы знаете, какого рода эта работа. Паника от пустого условия перестаёт приходить. На собеседовании меняется ещё одно. Правдоподобный подход в первую минуту покупает вам разговор. Четыре минуты молчания покупают подсказку, а подсказки стоят дорого. ## Где это применено Тренажёр распознавания это причина, по которой [AlgoPath](/ru) существует. 52 паттерна с их сигналами и соседями. 194 неподписанные задачи. Забег на время. Матрица, показывающая, какие пары путаете лично вы. Первые 33 шага бесплатны, и тренажёр работает внутри них. --- title: "Как подготовиться к алгоритмическому собеседованию" url: https://algopath.pro/ru/blog/kak-podgotovitsya-k-algoritmicheskomu-sobesedovaniyu language: ru summary: "Меньше задач, каждая используется четыре раза, распознавание тренируется отдельно. Что делать в недели перед собеседованием." updated: 2026-10-15 --- # Как подготовиться к алгоритмическому собеседованию **Короткий ответ.** Тренируйте по отдельности четыре навыка, которые проверяет собеседование: распознать тип задачи, вспомнить технику, учесть границы и написать код без багов. Обычная практика тренирует только код. Добавьте разминку на распознавание по 3 минуты, повторяйте задачу на 1, 4, 11 и 27 день и раз в неделю решайте вслух на время. ## Разделите четыре навыка Ответ на собеседовании требует четырёх вещей. **Распознавание.** Понять, задача какого типа перед вами. **Память.** Вспомнить, как такой тип решается. **Границы.** Пустой вход, один элемент, ответа нет. **Код.** Написать без багов. Обычная практика тренирует код. Задевает память. Остальные два оставляет случаю. Собеседования проваливаются на этих двух. Значит, тренируйте их отдельно. В этом весь метод. ## Тренируйте распознавание без решения Самая ценная привычка здесь. Стоит 3 минуты на задачу. Прочитайте условие. Напишите две строки. Задача какого типа. Какие слова вам это сказали. Проверьте. Угадали? Идите дальше, не решая. Запишите ошибку и то, чем она вам показалась. Потом решайте. Двадцать таких за час. Решить двадцать это день. Нужны задачи без подписи. Большинство сайтов сортирует по темам, и ответ висит над вопросом. Сигналы по каждому паттерну разобраны в статье [распознавание паттернов](/ru/blog/patterny-algoritmov). ## Используйте каждую задачу четыре раза Решили один раз, и работа испарилась. Лечится [интервальными повторениями](https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%82%D0%B5%D1%80%D0%B2%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D0%BF%D0%BE%D0%B2%D1%82%D0%BE%D1%80%D0%B5%D0%BD%D0%B8%D1%8F). Это самый надёжный результат во всей литературе об обучении. **День 1.** Решите. Плохо тоже нормально. **День 4.** Прочитайте условие. Назовите паттерн вслух. Без кода. **День 11.** Напишите ядро с пустого файла. Без заметок. **День 27.** Назовите паттерн меньше чем за 30 секунд. После провала шага откатитесь на один интервал назад. Цифры выглядят странно намеренно. Они стоят сразу за точкой, где начинает забываться. Усилие вытащить обратно и есть то, что укрепляет. ## Вспоминайте, а не перечитывайте Перечитывание своего старого решения ощущается продуктивным. Производить ответ вместо этого называется [эффектом тестирования](https://ru.wikipedia.org/wiki/%D0%AD%D1%84%D1%84%D0%B5%D0%BA%D1%82_%D1%82%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F). Почему он сильнее перечитывания, разобрано в статье [активное вспоминание](/ru/blog/aktivnoe-vspominanie). Знакомый текст даёт сильное чувство знания. Это чувство умирает перед пустым редактором. Каждое повторение должно что-то произвести. Пустой файл. Или проговорить вслух с закрытым экраном. Или написать подход на бумаге. Моё правило: ничего не произвёл, значит ничего не повторил. ## Таймер добавляйте поздно Собеседование это выступление на время перед незнакомым человеком. Если вся практика была спокойной, первой попыткой на время станет само собеседование. Когда включать таймер, разобрано в статье [волнение на собеседовании](/ru/blog/volnenie-na-sobesedovanii). Дождитесь, пока тема станет комфортной. Возьмите незнакомую задачу. Дайте себе 25 минут. Говорите вслух всё время, в пустую комнату. Ощущается нелепо. Интервьюер судит по мышлению, которое слышит. Что именно говорить, разобрано в статье [алгоритмическая секция](/ru/blog/algoritmicheskaya-sektsiya). Раз в неделю достаточно. ## Ведите матрицу путаницы При неверном названии паттерна запишите пару. Что было. Что вы сказали. После 30-40 записей проступает форма. Почти у каждого есть три-четыре пары, которые он путает. Два указателя против скользящего окна. [Жадность](https://ru.wikipedia.org/wiki/%D0%96%D0%B0%D0%B4%D0%BD%D1%8B%D0%B9_%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC) против [динамического программирования](https://ru.wikipedia.org/wiki/%D0%94%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5). BFS против DFS. Эти пары и есть ваш план занятий. Новые задачи в основном проверяют то, что вы и так делаете верно. ## Неделя, которая работает 3 занятия по 45 минут. Одно длинное даёт меньше. Каждое занятие: 10 минут распознавания, 25 минут на одну новую задачу, 10 минут повторения. Одна попытка на время с проговариванием в неделю. Воскресенье: 10 минут с матрицей. Выбрать фокус на следующую неделю. Меньше 3 часов в неделю. Четыре месяца такого бьют месяц по 6 часов в день. ## Что пропустить Списки «топ-150», пока нет паттернов. Список это порядок задач, а порядок не программа. Заучивание решений. Работает на одной задаче и ломается на соседней. Интервьюеры меняют одну деталь специально. Видео, где человек решает задачу, ответ которой уже знает. Если только вы не пробовали сами до этого. Почему сам материал ощущается враждебным, разобрано в статье [почему алгоритмы ненавидят](/ru/blog/nenavizhu-algoritmy). ## Где это применено [AlgoPath](/ru) работает по этому методу. 194 неподписанные задачи на распознавание. Очередь повторений по лестнице выше. Матрица путаницы из ваших собственных ошибок. Забег на время. Первые 33 шага бесплатны, и тренажёр работает внутри них. --- title: "Три новых языка: Добро пожаловать TypeScript, Java и C#!" url: https://algopath.pro/ru/blog/tri-novyh-yazyka-typescript-java-csharp language: ru summary: "TypeScript, Java и C# теперь работают в браузере рядом с JavaScript, Python и PHP. Стартер на каждом из шести есть во всех 143 упражнениях, и ставить ничего не нужно." updated: 2026-09-09 --- # Три новых языка: Добро пожаловать TypeScript, Java и C#! В AlgoPath появились три языка: - TypeScript - Java - C#. Вместе с JavaScript, Python и PHP их теперь 6. Стартер на каждом есть во всех 143 упражнениях. Всё выполняется в браузере. Ставить ничего не нужно, и код не уходит из вкладки. ## Кому это было нужно? Тем, кто готовится к собеседованиям. Вопросы там одни и те же. Неважно, на чём ты пишешь на работе. Раньше ответ был один: «решай на JavaScript или Python». Под таймером это плохой ответ. Минуты уходят на синтаксис, которым ты не пользуешься. ## Язык меняет саму задачу? Нет. Тесты во всех шести одинаковые. Меняется сигнатура. Кейсы нет. Реши шаг на Python сегодня. Перепиши его на Java через месяц. Планка та же. ## Подсказки на всех языках? Да. В каждой подсказке с кодом теперь есть блок под каждый язык. Это 1676 новых блоков. Каждый ответ подставляется в свой шаг. Потом прогоняется по тестам этого шага. Подсказка, которая не решает задачу, до тебя не доедет. Текст тоже переписан. 394 ступени объясняли план на JavaScript. Даже тому, кто пишет на Python. ## Где это в треке? Во всех 150 шагах. Шаг 19 хорош как первый. Два указателя, шесть кейсов. Три из них ждут null. Зачем трек существует, написано в статье [зачем я сделал AlgoPath](/ru/blog/zachem-ya-sdelal-algopath). Почему материал кажется враждебным, в статье [почему алгоритмы ненавидят](/ru/blog/nenavizhu-algoritmy). Первые 33 шага бесплатны целиком. Выбери язык над редактором и напиши. --- title: "Ненавижу алгоритмы: что на самом деле идёт не так" url: https://algopath.pro/ru/blog/nenavizhu-algoritmy language: ru summary: "Тот, кто говорит «ненавижу алгоритмы», обычно упирается в одну из четырёх стен. У каждой стены есть имя и решение." updated: 2026-10-15 --- # Ненавижу алгоритмы: что на самом деле идёт не так **Короткий ответ.** Алгоритмы обычно ненавидят по четырём причинам, и все четыре чинятся. Туториал показывает ответ раньше задачи. Никто не говорит, где этот код работает в продакшене. Сложность прыгает от циклов к задачам с собеседований через пропущенные шаги. Отметка «решено или нет» прячет, какой навык сломался. Каждое исправление занимает минуты в день. ## 1. Ответ показывают раньше вопроса Откройте любой туториал по [бинарному поиску](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA). Туториал начинается с названия. Дальше идёт код. Самое интересное уже произошло. Кто-то прочитал задачу и решил, что это бинарный поиск. Видео начинается после этого. Потом вам попадается настоящая. «Найдите минимальную грузоподъёмность, чтобы увезти все посылки за 10 дней». Ничто в этой фразе не похоже на отсортированный массив. **Что делать.** Перед чтением решения напишите одну строку. Что в условии должно было навести вас на этот путь? Неспособность написать строку значит, что вы просто прочитали задачу. ## 2. Никто не говорит, зачем этот код «Разверните [связный список](https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9_%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA)» это загадка. Я делал это несколько раз, прежде чем узнал, где оно работает. [LRU-кэш](https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC%D1%8B_%D0%BA%D1%8D%D1%88%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F) двигает использованную запись в начало. Undo идёт назад по цепочке. Планировщик вынимает задачу из очереди. Обычный вторник на работе. **Что делать.** Для каждого алгоритма найдите одну реальную систему, где он крутится. [git bisect](https://git-scm.com/docs/git-bisect) это бинарный поиск. [Rate limiter](https://ru.wikipedia.org/wiki/%D0%9E%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D0%B5_%D1%81%D0%BA%D0%BE%D1%80%D0%BE%D1%81%D1%82%D0%B8) это скользящее окно. Индексы в базе это сбалансированные деревья. Пять минут на каждый. Мелочь превращается в механизм. ## 3. Сложность прыгает Обычный путь: переменные, циклы, функции, задачи с собеседований. В этом разрыве прячется много. Почему обращение по индексу мгновенное. Что делает хеш-таблица. Почему «мгновенно» перестаёт быть правдой на 10 миллионах. Вслух этого никто не говорит. Задача, помеченная как Easy, превращается в вывод о себе. Вывод неверный. Вам дали сороковую ступень и назвали её пятой. **Что делать.** Когда что-то кажется невозможным, ищите пропущенный шаг. Обычно это одна идея, и она меньше, чем выглядит. ## 4. Оценка не говорит ничего Сайт с задачами выдаёт один бит. Решено или нет. А сломаться могло четыре разных вещи. Распознавание паттерна. Память о технике. Граничные случаи. Написание кода без багов. Красный крест прячет, что именно. Чинить нечего, вы открываете следующую задачу. Люди месяцами гриндят и стоят на месте. **Что делать.** После каждой задачи записывайте, какая из четырёх подвела. Две недели этого стоят больше сотни новых задач. Какой способ практики что тренирует, расписано на странице [как правильно учить алгоритмы](/ru/best-way-to-practice-dsa). Неделя под эти четыре исправления: [как готовиться к алгоритмам](/ru/blog/kak-podgotovitsya-k-algoritmicheskomu-sobesedovaniyu). ## Что правда сложно Часть алгоритмов действительно тяжёлая. Динамическое программирование займёт время при любой подаче. Основная тоска идёт от порядка подачи. Спрятать рассуждение. Убрать смысл. Пропустить фундамент. И мерить одним битом. Почините эти четыре вещи, и стена исчезнет. Месяцы всё равно уйдут. Просто перестанет быть противно, а это и решает, дойдёте ли вы. Развёрнутая версия того, почему мне не всё равно, в статье [зачем я сделал AlgoPath](/ru/blog/zachem-ya-sdelal-algopath). ## Частые вопросы ### Нормально ли ненавидеть алгоритмы? Да. Большая часть неприязни идёт от четырёх проблем выше, и каждая чинится за несколько минут в день. Динамическое программирование остаётся тяжёлым почти для всех. ### Почему алгоритмы и структуры данных даются так тяжело? Чаще всего из-за пропущенных шагов. Курсы прыгают от циклов к задачам с собеседований и пропускают, например, почему обращение к элементу массива мгновенное. Пять вещей, которые пропускают чаще всего, разобраны в статье [как научиться решать алгоритмические задачи](/ru/blog/kak-nauchitsya-reshat-algoritmicheskie-zadachi). ### Почему многие не любят LeetCode? Он оценивает задачу одним битом: решил или нет. Какой навык подвёл, он не говорит. Метка Easy прячет пропущенные шаги, и проваленная лёгкая задача ощущается как приговор. Записывайте после каждой задачи, какой из четырёх навыков сломался, и большая часть этого уйдёт. ## Где это применено [AlgoPath](/ru) построен на этих четырёх исправлениях. Сигналы идут раньше решения. Каждый шаг заканчивается реальным продакшн-случаем. Лестница начинается с урока [«что такое алгоритм»](/ru/steps/what-is-an-algorithm). Тренажёр показывает, какие паттерны вы путаете, а тесты шага показывают, на каком граничном случае сломался код. Первые 33 шага бесплатны. --- title: "Зачем я сделал AlgoPath" url: https://algopath.pro/ru/blog/zachem-ya-sdelal-algopath language: ru summary: "Я месяцами решал задачи и всё равно зависал на собеседованиях. Вот чего не хватало и что я сделал вместо этого." updated: 2026-09-09 --- # Зачем я сделал AlgoPath Я умел писать код. Я делал вещи, которыми пользовались живые люди. И всё равно сидел перед задачей с собеседования. И не понимал, что делать. Ради этого разрыва и сделан AlgoPath. ## Что на самом деле шло не так? Я делал то, что советуют все. Взять задачу, застрять, прочитать решение, успокоиться. Потом застрять на следующей. Задаче на шестидесятой я заметил неприятное. Я не становился лучше в решении. Я становился лучше в чтении решений. На собеседовании проверяют только первое. Почему привычка появляется, разобрано в статье [почему алгоритмы ненавидят](/ru/blog/nenavizhu-algoritmy). ## Когда это стало очевидно? В задаче просили найти самый длинный участок под лимитом. Я смотрел на неё 20 минут. Ответом было [скользящее окно](/ru/patterns/sliding-window-variable). На той же неделе я решил четыре таких задачи. Написать его я умел. Не умел я его увидеть. Этому шагу меня никто не учил. Все туториалы начинались после него. ## Какой шаг никто не объясняет? Решение алгоритмической задачи состоит из трёх частей. Понять, какого задача типа. Вспомнить, как решается этот тип. Написать код. Курсы учат второму и третьему. Сайты с задачами дают третье. Первое решает собеседование, и его оставляют на самотёк. Само оно правда появляется. Уходит на это несколько сотен задач. Большинство бросает раньше. Как тренировать это намеренно, разобрано в статье [распознавание паттернов](/ru/blog/patterny-algoritmov). ## Что из этого выросло? 150 шагов по порядку. Каждый берёт одну идею, объясняет её и заставляет написать самому. Дальше она попадает в настоящую систему. Рейт-лимитер, LRU-кеш, автодополнение. Рядом: 52 паттерна, у каждого сигналы, которые на него указывают. И 194 задачи без подписи. Паттерн называешь до первой строчки кода. Из ответов собирается карта того, что ты с чем путаешь. Полезнее инструмента у меня при учёбе не было. ## Как устроен один шаг? Прочитать идею. Разобрать сигналы до кода, намеренно. Написать ядро самому в браузере: JavaScript, Python или PHP. Дальше подключить его к настоящему кейсу. Шаг засчитан, когда объясняешь его без заметок. И пишешь ядро заново с чистого файла. Это [эффект тестирования](https://ru.wikipedia.org/wiki/%D0%AD%D1%84%D1%84%D0%B5%D0%BA%D1%82_%D1%82%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F), он и проверяет. Потом шаг возвращается. Через несколько дней, через пару недель, через месяц. Обычные [интервальные повторения](https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%82%D0%B5%D1%80%D0%B2%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5_%D0%BF%D0%BE%D0%B2%D1%82%D0%BE%D1%80%D0%B5%D0%BD%D0%B8%D1%8F). ## Зачем каждый урок написан трижды? Глубину выбираешь сам, на каждом шаге. Простой ничего не требует заранее. Стандартный говорит языком работающего разработчика. Академический даёт инварианты и точную сложность. Курс только для новичков бросают все остальные. Написанный как надо теряет новичков на первой странице. ## Кому это нужно? Тем, кто умеет написать цикл и функцию. И встаёт, как только задача перестаёт объявлять, чего она хочет. Сменившим профессию, самоучкам, людям с парой лет работы и без формальной учёбы. Я из последней группы. Для спортивного программирования это не подходит. И собеседование на следующей неделе не спасёт. ## Почему это пишу я? Меня зовут Эльман Гусейнов. Десять лет разработчиком. Сейчас Zalando в Берлине, до этого Shopify и Scoutbee. Подробнее на [helloelman.com](https://helloelman.com). Ничто из этого не помешало мне зависать. Вот это и стоит сказать вслух. Я никогда не учил computer science. Всё, что знаю, набрано по ходу работы. Это эффективно и оставляет дыры. Это была одна из них. Я строил курс, пока учился сам. Каждое место, где объяснение замедляется, это место, где застрял я. ## С чего начать? Первые 33 шага бесплатны целиком. Big O, хеш-таблицы, два указателя, скользящее окно, бинарный поиск. Начинай с первого шага, если эти слова незнакомы. Пройди [вступительный тест](/ru/placement), если код ты уже пишешь. Семь минут и без аккаунта. Вся карта лежит на [странице курса](/ru/roadmap). AlgoPath живёт на [algopath.pro](/ru). ## Страницы сайта --- title: "AlgoExpert или AlgoPath: стоит ли платить за AlgoExpert" url: https://algopath.pro/ru/algopath-vs-algoexpert language: ru updated: 2026-10-22 --- # AlgoExpert или AlgoPath: стоит ли платить за AlgoExpert Коротко. AlgoExpert это отобранный список из 200 задач с собеседований, к каждой есть видеоразбор. AlgoPath заставляет написать сам алгоритм до того, как ты увидишь чьё-то решение. Нужен один аккуратный список с видео? AlgoExpert делает это хорошо. Понимаешь любой разбор и всё равно не можешь написать код сам? Начни здесь. *Цены проверены на странице самого сервиса 26 сентября 2026 года.* ## Рядом | | AlgoExpert | AlgoPath | | --- | --- | --- | | Единица работы | Одна задача с собеседования | Один алгоритм | | Что получается на выходе | Решение этой задачи | Алгоритм, потом система, которая его использует | | Объяснения | Видео из двух частей к каждой задаче | Теория текстом и лестница подсказок, которая подталкивает, а не отвечает | | Языки | Письменные решения на 9 языках | Шесть, прямо в браузере: JS, TS, Python, PHP, Java, C# | | Практика на время | 4 экзамена на время | Раунд на время с незнакомой задачей | | Цена | $119 за год All Access, 7 продуктов | Первые 33 шага бесплатно, потом один платёж | ## Где AlgoExpert сильнее Видео. К каждой задаче есть разбор в две части: сначала идея, потом код. Пакет. Те же $119 открывают ещё шесть продуктов, в том числе системный дизайн. В AlgoPath системного дизайна нет совсем. Список короткий намеренно. 200 задач можно реально пройти до конца. ## Стоит ли AlgoExpert своих денег Да, если ты учишься, глядя, как рассуждает другой человек, и хочешь один список. Цена покрывает все их продукты, и это честная сделка, если пользуешься хотя бы двумя. Хуже, если у тебя проблема с воспроизведением. Видео показывает ответ раньше, чем ты свой написал, и задача, которую посмотрел, кажется решённой, хотя это не так. На странице рядом с ценой зачёркнуто $693 и идёт обратный отсчёт. Оценивай сами $119. ## Где AlgoPath сильнее Каждый шаг выдаёт файл с сигнатурой, TODO и тестами. Тело функции пишешь ты. Тесты остаются красными, пока код не заработает. Потом та же функция уходит в маленький продакшн-кейс: ограничитель запросов, LRU-кеш, git bisect. Ты видишь, как алгоритм делает работу. Отдельный тренажёр качает то, что собеседование проверяет первым. Ты читаешь задачу без подписи и называешь один из [52 паттернов](/ru/patterns) до того, как написал строчку. ## Как совмещать Сначала собери алгоритм здесь. Потом возьми задачу из AlgoExpert, где он нужен, и реши её с нуля. Видео смотри только после своей попытки. ## Итог - Один список, видео и системный дизайн в одной покупке: AlgoExpert. - Понимаешь любой разбор, но не пишешь код сам: AlgoPath. - Нужны сотни повторений и задачи по компаниям: [LeetCode Premium](/ru/is-leetcode-premium-worth-it). - Блок-схема паттернов и проверенные шаблоны кода: [AlgoMonster](/ru/algopath-vs-algomonster). ## Попробуй [Тест на уровень](/ru/placement) занимает 15 вопросов и не требует аккаунта. Первые 33 шага бесплатны, среди них [префиксные суммы](/ru/steps/prefix-sums) и [подсчёт отрезков с суммой k](/ru/steps/subarray-sum-k). [Полный доступ](/ru/pricing) оплачивается один раз, без продления. Выбираешь из большего числа вариантов? Вот [аналоги LeetCode рядом](/ru/leetcode-alternatives). --- title: "AlgoMonster или AlgoPath: стоит ли платить за AlgoMonster" url: https://algopath.pro/ru/algopath-vs-algomonster language: ru updated: 2026-10-22 --- # AlgoMonster или AlgoPath: стоит ли платить за AlgoMonster Коротко. И AlgoMonster, и AlgoPath учат паттернам. AlgoMonster даёт блок-схему, которая ведёт от задачи к алгоритму, и шаблон кода к каждому. AlgoPath заставляет написать каждый алгоритм из TODO и тренирует называть паттерн без схемы перед глазами. Нужен справочник, чтобы подглядывать? AlgoMonster. Нужно выдавать и паттерн, и код по памяти? Начни здесь. *Проверено на странице самого сервиса 26 сентября 2026 года. AlgoMonster показывает цену только после входа, поэтому здесь её нет.* ## Рядом | | AlgoMonster | AlgoPath | | --- | --- | --- | | Как выбирается паттерн | Блок-схема из вопросов «да или нет» о задаче | Тренажёр: назови паттерн для задачи без подписи и обоснуй | | Как пишется код | По шаблону для каждого паттерна | Из TODO, с тестами, которые красные, пока код не заработает | | Дополнительно | Режим спидрана, визуализатор алгоритмов, другие курсы в той же подписке | Продакшн-кейс в каждом шаге, раунд на время | | Бесплатная часть | Можно начать бесплатно | Первые 33 шага | | Цена | Видна после входа | Один платёж, без продления | ## Где AlgoMonster сильнее Блок-схема. Это хорошая карта того, какой вопрос задать задаче, и она открыта всем. Шаблоны. К каждому паттерну есть проверенный код, так что ошибка на единицу менее вероятна. Подписка покрывает не только алгоритмы. Проектирование классов входит в неё же. ## Стоит ли AlgoMonster своих денег Да, если хочешь одно место, которое подскажет, за какой паттерн взяться, и даст к нему проверенный код. В их собственном FAQ сказано, что схему не нужно заучивать. На собеседовании её нет на экране, поэтому решает умение пройти её в голове. У шаблона то же ограничение: он помогает, пока открыт рядом. Войди и посмотри цену до того, как решишь. На открытой странице её нет. ## Где AlgoPath сильнее Каждый шаг выдаёт файл с сигнатурой, TODO и тестами. Тело функции пишешь ты. Тесты остаются красными, пока код не заработает. Потом та же функция уходит в маленький продакшн-кейс: ограничитель запросов, LRU-кеш, git bisect. Ты видишь, как алгоритм делает работу. Отдельный тренажёр качает то, что собеседование проверяет первым. Ты читаешь задачу без подписи и называешь один из [52 паттернов](/ru/patterns) до того, как написал строчку. После десяти таких задач ты получаешь список того, какие паттерны с какими путаешь. ## Как совмещать Прочитай блок-схему AlgoMonster один раз, чтобы увидеть, какие вопросы она задаёт. Потом тренируйся здесь, пока не начнёшь задавать их без неё. ## Итог - Справочник по паттернам и проверенные шаблоны: AlgoMonster. - Назвать паттерн и написать код, когда ничего не открыто: AlgoPath. - Видео к каждой задаче: [NeetCode](/ru/algopath-vs-neetcode). - Анимации и сообщество: [Structy](/ru/algopath-vs-structy). ## Попробуй [Тест на уровень](/ru/placement) занимает 15 вопросов и не требует аккаунта. Первые 33 шага бесплатны, среди них [бинарный поиск границы](/ru/steps/binary-search-boundary) и [поиск в повёрнутом массиве](/ru/steps/search-rotated). [Полный доступ](/ru/pricing) оплачивается один раз, без продления. Выбираешь из большего числа вариантов? Вот [аналоги LeetCode рядом](/ru/leetcode-alternatives). --- title: "Structy или AlgoPath: стоит ли платить за Structy" url: https://algopath.pro/ru/algopath-vs-structy language: ru updated: 2026-10-22 --- # Structy или AlgoPath: стоит ли платить за Structy Коротко. Structy учит каждую тему через цепочку задач, к каждой есть анимированное объяснение и разбор кода. AlgoPath заставляет собрать сам алгоритм, а потом применить его в работающей системе. Хочешь, чтобы тебя вели от задачи к задаче? Structy это умеет. Разборы перестали оседать в голове? Начни здесь. *Цены проверены на странице самого сервиса 26 сентября 2026 года.* ## Рядом | | Structy | AlgoPath | | --- | --- | --- | | Единица работы | Задача внутри темы | Один алгоритм | | Что получается на выходе | Решение этой задачи | Алгоритм, потом система, которая его использует | | Объяснения | Анимация и разбор кода к каждой задаче | Теория текстом и лестница подсказок, которая подталкивает, а не отвечает | | Сообщество | Discord для участников | Пока нет | | Бесплатная часть | Курс можно начать бесплатно | Первые 33 шага | | Цена | $49 в месяц или $129 в год | Один платёж, без продления | ## Где Structy сильнее Анимации. Указатель, который едет по массиву, проще понять на картинке, чем по описанию. Разборы привязаны к языку. Ты видишь решение на том языке, на котором будешь проходить собеседование. К задаче идут её варианты, так что одну идею отрабатываешь несколько раз подряд. Есть сообщество. У AlgoPath нет Discord. ## Стоит ли Structy своих денег Начни с бесплатной части. Если анимации тебе помогают, $129 за год это нормальная цена за остальное. Помесячно плати, только если собеседование скоро. Три месяца по $49 уже дороже года. Хуже, если ты понимаешь каждый разбор и всё равно не пишешь код сам. Разбор это всё ещё кто-то другой, кто пишет за тебя. ## Где AlgoPath сильнее Каждый шаг выдаёт файл с сигнатурой, TODO и тестами. Тело функции пишешь ты. Тесты остаются красными, пока код не заработает. Потом та же функция уходит в маленький продакшн-кейс: ограничитель запросов, LRU-кеш, git bisect. Ты видишь, как алгоритм делает работу. Отдельный тренажёр качает то, что собеседование проверяет первым. Ты читаешь задачу без подписи и называешь один из [52 паттернов](/ru/patterns) до того, как написал строчку. В Structy задачи сгруппированы по темам, так что тема подсказывает паттерн раньше, чем ты должен его найти. ## Как совмещать Посмотри анимацию Structy по теме, которую трудно представить. Потом закрой её и собери алгоритм здесь из TODO. Анимация даёт картинку, а тесты показывают, можешь ли ты написать код. ## Итог - Лучше всего учишься по анимациям и хочешь сообщество: Structy. - Разборы понимаешь, а на пустом файле замираешь: AlgoPath. - Нужен один отобранный список с видео: [AlgoExpert](/ru/algopath-vs-algoexpert). - Блок-схема паттернов и шаблон кода к каждому: [AlgoMonster](/ru/algopath-vs-algomonster). ## Попробуй [Тест на уровень](/ru/placement) занимает 15 вопросов и не требует аккаунта. Первые 33 шага бесплатны, среди них [подсчёт через хеш-таблицу](/ru/steps/hashing-map-set-frequency) и [скользящее окно переменной длины](/ru/steps/sliding-window-variable). [Полный доступ](/ru/pricing) оплачивается один раз, без продления. Выбираешь из большего числа вариантов? Вот [аналоги LeetCode рядом](/ru/leetcode-alternatives). --- title: "Стоит ли покупать LeetCode Premium" url: https://algopath.pro/ru/is-leetcode-premium-worth-it language: ru updated: 2026-10-22 --- # Стоит ли покупать LeetCode Premium Коротко. LeetCode Premium стоит своих денег, если ты знаешь, в какую компанию идёшь, и паттерны у тебя уже уверенные. Он добавляет задачи с пометкой компании к самому большому банку задач. Написать алгоритм, который ты раньше не писал, он не учит. *Цены проверены на странице самого сервиса 26 сентября 2026 года.* ## Сколько стоит $35 в месяц с оплатой помесячно или $159 в год. Годовой план выходит $13.25 в месяц. Рядом на странице зачёркнуты $39 и $299. Оценивать стоит $35 и $159. ## Что добавляет Premium | Бесплатно | Premium | | --- | --- | | Большая часть банка задач | Все задачи, включая закрытые | | Обсуждения к каждой задаче | Вопросы с собеседований конкретных компаний | | Еженедельные контесты | Кодинг-агент с 500 дополнительными кредитами в месяц и до 30 разборов решений в день | ## Кому платить У тебя собеседование в названной компании через месяц-два. Задачи по компаниям трудно найти где-то ещё. Ты уже узнаёшь частые паттерны, и нужен объём. Задач здесь больше, чем где-либо. Условия там только на английском, и как их читать, разобрано в статье [LeetCode на русском](/ru/blog/leetcode-na-russkom). Под одно собеседование купи один месяц. Год бери, только если будешь пользоваться дольше пяти месяцев: пять месяцев по $35 уже больше $159. ## Кому не стоит Ты открываешь задачу и замираешь. Больше задач это не исправит. Не хватает распознавания, а банк задач ничего за тебя не подписывает. Ты называешь правильный подход, а код выходит сломанным. Это воспроизведение. Premium даёт больше мест, где на этом споткнуться, и никакого пути через это. Как тренировать память рук, распознавание и скорость по отдельности, расписано на странице [как правильно учить алгоритмы](/ru/best-way-to-practice-dsa). ## Что AlgoPath делает вместо этого Каждый шаг выдаёт файл с сигнатурой, TODO и тестами. Тело функции пишешь ты. Тесты остаются красными, пока код не заработает. Потом та же функция уходит в маленький продакшн-кейс: ограничитель запросов, LRU-кеш, git bisect. Ты видишь, как алгоритм делает работу. Отдельный тренажёр качает то, что собеседование проверяет первым. Ты читаешь задачу без подписи и называешь один из [52 паттернов](/ru/patterns) до того, как написал строчку. Потом возвращайся на LeetCode, бесплатный или платный, и используй его для того, в чём он лучше всех: для объёма. ## Итог - Собеседование в названной компании скоро, паттерны уверенные: один месяц Premium. - Замираешь на незнакомых задачах: сначала тренируй распознавание. - Понимаешь решения, но не пишешь их: собирай алгоритмы руками. - Видео к каждой задаче из списка: [NeetCode](/ru/algopath-vs-neetcode), смотреть бесплатно. ## Попробуй [Тест на уровень](/ru/placement) занимает 15 вопросов и не требует аккаунта. Первые 33 шага бесплатны, среди них [скользящее окно фиксированной длины](/ru/steps/sliding-window-fixed) и [разностный массив](/ru/steps/difference-arrays). [Полный доступ](/ru/pricing) оплачивается один раз, без продления. Выбираешь из большего числа вариантов? Вот [аналоги LeetCode рядом](/ru/leetcode-alternatives). --- title: "Datenschutz" url: https://algopath.pro/ru/datenschutz language: ru updated: 2026-10-02 --- # Datenschutz Здесь описано, какие персональные данные обрабатывает AlgoPath, зачем и какие у вас права. Основание: Общий регламент ЕС по защите данных (GDPR). ## Ответственный за обработку данных algopath.pro - Elman Huseynov c/o Online-Impressum #9942 Europaring 90 53757 Sankt Augustin, Deutschland Эл. почта: contact@elman.group ## Хостинг Сайт размещён на общем веб-хостинге Hostinger International Ltd. Провайдер обрабатывает описанные ниже журналы сервера по нашему поручению на основании договора об обработке данных. ## Данные аккаунта Вы входите либо через Google или GitHub, либо по адресу эл. почты и паролю. При входе через Google или GitHub мы получаем от провайдера ваше имя и адрес почты; пароль от этих сервисов мы не получаем. При входе по паролю он хранится только в виде криптографического хеша. По желанию вы можете дополнить профиль: отображаемое имя и ссылки на ваши профили в других сервисах. Это добровольно и для пользования курсом не требуется. Правовое основание: исполнение договора (ст. 6 (1)(b) GDPR); для необязательных полей профиля действует ваше согласие (ст. 6 (1)(a) GDPR). ## Прогресс обучения Мы храним, какие шаги вы открыли, сдали и завершили, серию активных дней, даты повторений и ваше место в рейтинге. Это нужно, чтобы курс продолжался между визитами. Правовое основание: исполнение договора (ст. 6 (1)(b) GDPR). ## Оплаты Платный доступ проводит Stripe, выступая продавцом от своего имени (merchant of record). Платёжные данные вы вводите только в Stripe. Мы никогда не видим и не храним номер карты. По покупке мы храним: сумму, валюту, идентификатор платежа в Stripe, страну платёжного адреса и сумму налога, указанную Stripe. Страна и налог нужны нам для собственной бухгалтерии. Перед оплатой мы спрашиваем ваше согласие на немедленное начало исполнения. По этому согласию мы сохраняем время и IP-адрес, поскольку должны иметь возможность доказать досрочное прекращение права на отзыв. Правовое основание: исполнение договора (ст. 6 (1)(b) GDPR) и правовая обязанность (ст. 6 (1)(c) GDPR) в части хранения документов. Политика Stripe: https://stripe.com/privacy. ## Отзыв договора Если вы пользуетесь кнопкой «Vertrag widerrufen», мы сохраняем ваше имя, адрес эл. почты, номер заказа, а также время подачи и подтверждения. Причину мы не спрашиваем. Правовое основание: правовая обязанность (ст. 6 (1)(c) GDPR). ## Формы и сообщения То, что вы отправляете через форму обратной связи и форму сообщения об ошибке, сохраняется для обработки обращения. К сообщению об ошибке можно по желанию приложить снимок экрана; изображения перекодируются при загрузке, поэтому встроенные метаданные теряются. Для рассылки мы храним адрес почты, пока вы не отпишетесь. В каждом письме есть ссылка на отписку. Правовое основание: законный интерес в ответе на обращения (ст. 6 (1)(f) GDPR); для рассылки действует ваше согласие (ст. 6 (1)(a) GDPR). ## Код, который вы пишете Ваши решения задач выполняются полностью в браузере через WebAssembly. Код не отправляется на наши серверы и не выполняется на них. Если вы сохраняете решение, хранится его текущее состояние, чтобы вы могли продолжить позже. ## Рассылка Для рассылки мы храним ваш адрес электронной почты, имя и момент, когда вы дали согласие. Момент мы храним потому, что должны уметь подтвердить согласие (§ 7 абз. 2 № 2 UWG). Мы также записываем, какое письмо и когда вам ушло, иначе одно и то же письмо могло бы прийти вам дважды. Правовое основание: ваше согласие (ст. 6 (1)(a) GDPR). Отозвать его можно в любой момент: ссылкой об отписке в каждом письме или галочкой в профиле. Законность обработки до отзыва это не затрагивает. Письма уходят с нашего собственного сервера у Hostinger; внешний сервис рассылок мы не используем. Если измерение доставки включено, в письме есть пиксель 1×1 и ссылки, которые ведут через наш сервер: так мы видим, открыли ли письмо и нажали ли ссылку. Эти цифры приблизительные, и мы относимся к ним именно так: тот, кто блокирует картинки, никогда не попадёт в число открывших, а некоторые почтовые программы подгружают картинки сами, хотя письмо никто не читал. Если запретить загрузку внешних картинок в почтовой программе, измерение открытий не произойдёт. Если измерение выключено, в письмах нет ни пикселя, ни перенаправленных ссылок. ## Файлы cookie и согласие Необходимые cookie обеспечивают работу сайта: сессионный cookie держит вас авторизованным, ещё один запоминает выбранный язык, а cookie «cookie_consent» хранит ваш выбор в баннере согласия. Для них согласие не требуется (ст. 6 (1)(f) GDPR). Рекламных cookie мы не используем. Аналитические cookie ставятся, только если вы согласитесь в баннере (см. раздел «Веб-аналитика»). Формы обратной связи и сообщения об ошибке используют Google reCAPTCHA для защиты от спама. reCAPTCHA относится к необязательным сторонним службам, поэтому мы загружаем её только после вашего согласия в баннере. Если вы откажетесь, reCAPTCHA не загружается, а формы защищаются скрытым полем и ограничением частоты запросов. Правовое основание для reCAPTCHA: ваше согласие (ст. 6 (1)(a) GDPR). Отозвать согласие можно в любой момент, удалив cookie «cookie_consent» в браузере. Тогда баннер появится снова. Провайдер: Google Ireland Ltd, см. https://policies.google.com/privacy. ## Открытый рейтинг В AlgoPath есть рейтинг участников. Тот, кто прошёл хотя бы один шаг, показан в нём под именем и с картинкой из своего аккаунта, вместе с набранными очками, числом пройденных шагов и достигнутым уровнем. То же самое видно на карточке участника. И то, и другое открывается без аккаунта. Поисковым системам запрещено включать эти страницы в индекс. Это не мешает посетителю их прочитать. В профиле можно в любой момент отказаться («Скрыть меня из рейтинга»). После этого вас нет ни в таблице, ни в её местах, а карточка участника перестаёт открываться. На собственный прогресс это никак не влияет. Правовое основание: наш законный интерес в наглядном сравнении между учащимися (ст. 6 (1)(f) GDPR) вместе с возможностью отказаться в любой момент. ## Источники переходов Мы считаем, откуда приходят посетители, чтобы понимать, какие наши публикации и ссылки действительно приводят людей. Это наше собственное измерение, без третьих сторон и без дополнительных cookie. Один раз за сессию мы сохраняем одну запись: название источника (например, «reddit»), тип ссылки (соцсеть, поиск, переход, прямой заход), название кампании, если ссылка его содержала, домен, с которого пришёл визит (только домен, никогда полный адрес), первую открытую страницу и время. IP-адрес мы не сохраняем, user-agent браузера тоже не сохраняем. Запись привязана только к вашей текущей сессии, которая заканчивается через два часа бездействия; более поздний визит считается новым и не связанным с прежним. Если во время этой сессии вы создадите аккаунт, запись будет связана с аккаунтом, чтобы мы видели, из какого канала он пришёл. Эту связь можно удалить по запросу; вместе с удалением аккаунта она удаляется в любом случае. Правовое основание: наш законный интерес понимать, какие из наших публикаций работают (ст. 6 (1)(f) GDPR). Согласие для этого не требуется, поскольку кроме технически необходимого сессионного cookie на вашем устройстве ничего не сохраняется и не считывается. Анонимные записи удаляются через 13 месяцев. ## Веб-аналитика Если веб-аналитика включена, мы загружаем Google Analytics 4 (Google Ireland Ltd., Gordon House, Barrow Street, Dublin 4, Ирландия), и только после того, как вы согласились с необязательными cookie в баннере. Если вы откажетесь или аналитика выключена, ничего не загружается и cookie аналитики не ставится. Обрабатываются: открытые страницы, время и длительность визита, примерное местоположение по укороченному IP-адресу, тип устройства, браузер и страница, с которой вы пришли. Google Analytics 4 не хранит IP-адрес. Контейнер Google Tag Manager мы не используем, чтобы через него нельзя было подгрузить другие сторонние скрипты. Правовое основание: ваше согласие (ст. 6 (1)(a) GDPR, § 25 (1) TTDSG). Отозвать его можно в любой момент, удалив cookie «cookie_consent» в браузере, тогда баннер появится снова. Политика Google: https://policies.google.com/privacy. ## Мониторинг ошибок Если на сервере происходит сбой, приложение сообщает о нём в Sentry (Functional Software, Inc., через Sentry GmbH, Берлин), чтобы мы могли его исправить. Передаются технический отчёт об ошибке, запрошенный маршрут и время. Мы используем европейский регион Sentry, поэтому отчёты об ошибках обрабатываются в ЕС. Персональные данные при этом не отправляются: опция send_default_pii выключена, поэтому ни IP-адрес, ни данные аккаунта, ни содержимое форм не передаются. Полностью исключить, что в отдельном сообщении об ошибке окажутся персональные данные, нельзя. Правовое основание: законный интерес в работоспособности и безопасности сервиса (ст. 6 (1)(f) GDPR). Политика Sentry: https://sentry.io/privacy. ## Журналы сервера Хостинг-провайдер автоматически записывает стандартные данные доступа (IP-адрес, дату и время, запрошенную страницу, браузер и операционную систему). Эти данные нужны для безопасности и работы сервиса и не объединяются с другими данными о вас. Правовое основание: законный интерес (ст. 6 (1)(f) GDPR). ## Получатели данных Google (вход, reCAPTCHA в формах и, при вашем согласии, веб-аналитика), GitHub (вход), Stripe (оплаты) и Hostinger (хостинг). Каждый обрабатывает данные по своей политике конфиденциальности. Мы передаём только то, что нужно каждому сервису для его функции. ## Передача данных в США Google, GitHub и Stripe входят в группы компаний со штаб-квартирами в США, поэтому данные могут попадать туда. Основанием служит решение Еврокомиссии об адекватности от 10 июля 2023 года по EU-US Data Privacy Framework, если получатель в нём сертифицирован, а в остальных случаях стандартные договорные условия по ст. 46 (2)(c) GDPR. Даже при таких основаниях полностью исключить доступ американских органов власти нельзя. Шрифты и все прочие элементы оформления сайта загружаются с нашего собственного сервера, поэтому при простом открытии страницы к третьим лицам ничего не уходит. ## Сроки хранения Данные аккаунта и прогресса хранятся, пока существует ваш аккаунт. Документы о покупках и отзывах хранятся столько, сколько требуют налоговое и торговое право. При удалении аккаунта мы удаляем ваши персональные данные, кроме тех, которые обязаны хранить по закону. ## Ваши права У вас есть право на доступ, исправление, удаление, ограничение обработки и переносимость данных, а также право возражать против обработки. Чтобы воспользоваться любым из них, напишите на contact@elman.group. Вы также вправе подать жалобу в надзорный орган по защите данных. В Берлине это Berliner Beauftragte für Datenschutz und Informationsfreiheit. --- title: "Условия" url: https://algopath.pro/ru/agb language: ru updated: 2026-09-13 --- # Условия Эти условия регулируют покупку и использование полного доступа к курсу AlgoPath. Они действуют и для частных лиц, и для компаний. ## 1. Оператор algopath.pro - Elman Huseynov c/o Online-Impressum #9942 Europaring 90 53757 Sankt Augustin, Deutschland Эл. почта: contact@elman.group USt-IdNr.: DE459148722 ## 2. Что продаётся AlgoPath является онлайн-курсом по структурам данных и алгоритмам: текстовые уроки, упражнения с автоматическими тестами и раннер, который запускает ваши решения прямо в вашем браузере. Несколько первых шагов курса бесплатны, покупка для них не нужна. Полный доступ открывает упражнения всех остальных шагов. Доступ действует 12 месяцев с момента открытия. Он не продлевается автоматически, и ничего больше не списывается: подписки нет, продлевать и отменять нечего. После 12 месяцев оператор оставляет доступ открытым, пока курс существует; права требовать этого нет. ## 3. Цена и продавец Цена на странице тарифа окончательная. Налог уже включён, при оплате ничего не добавляется. Платёж проводит Stripe как продавец (merchant of record). По самому платежу вашим контрагентом выступает Stripe: он выдаёт чек, собирает и перечисляет налог. Доступ к курсу предоставляет оператор, указанный выше. Оплата вносится целиком при оформлении. Доступ открывается автоматически после подтверждения платежа со стороны Stripe. ## 4. Как заключается договор Цена, показанная на сайте, является приглашением к покупке, а не обязывающей офертой. Обязывающий заказ вы делаете, подтверждая покупку на странице оплаты и проводя платёж. Договор считается заключённым в момент, когда доступ открывается вашему аккаунту; это происходит автоматически после подтверждения платежа. Текст договора не сохраняется в виде, который можно потом запросить. Сохраните письмо-подтверждение и чек от Stripe. ## 5. Право на отзыв Если вы потребитель, у вас есть законное право отказаться от договора в течение 14 дней. Подробности и кнопку для отзыва вы найдёте на странице Widerrufsbelehrung. Это право прекращается досрочно, если вы прямо потребовали открыть доступ немедленно и подтвердили, что тем самым теряете право на отзыв. Страница оплаты спрашивает именно об этом до платежа и сохраняет ваш ответ. ## 6. Аккаунт и использование Доступ персональный. Не передавайте аккаунт и материалы курса другим людям и не публикуйте уроки и упражнения заново. Ваши решения принадлежат вам. Написанный вами код выполняется в вашем браузере и никогда не запускается на наших серверах. Доступ может быть отозван без возврата денег, если аккаунтом пользуются несколько человек, материалы распространяются дальше или платформу атакуют. ## 7. Доступность Курс продаётся в том виде, в каком он существует на момент покупки. Материалы добавляются и правятся со временем; это входит в купленный доступ и отдельно не оплачивается. Оператор может закрыть курс после того, как ваши 12 месяцев истекли. Если курс закрывается раньше, цена возвращается пропорционально оставшемуся времени. Непрерывную доступность гарантировать нельзя. Короткие перерывы на обслуживание, сбои хостинга и причины вне контроля оператора не дают права на возврат. ## 8. Ответственность Оператор отвечает без ограничений за умысел, грубую неосторожность и за вред жизни, телу и здоровью. При обычной неосторожности ответственность ограничена нарушением обязанностей, существенных для цели договора, и типичным предвидимым ущербом. Курс является учебным материалом. Никаких обещаний о результате собеседования, экзамена или найма не даётся. ## 9. Разрешение споров с потребителями Мы не обязаны и не готовы участвовать в процедуре разрешения споров в согласительной комиссии по делам потребителей. ## 10. Применимое право Применяется право Германии, за исключением Венской конвенции о договорах международной купли-продажи товаров. Если вы потребитель, проживающий в ЕС, обязательные нормы защиты прав потребителей вашей страны сохраняют силу. Если какое-то положение этих условий окажется недействительным, остальные продолжают действовать. --- title: "Widerrufsbelehrung" url: https://algopath.pro/ru/widerrufsbelehrung language: ru updated: 2026-07-31 --- # Widerrufsbelehrung Право потребителя на отзыв договора. Потребителем считается любое физическое лицо, заключающее договор в целях, не связанных преимущественно с его предпринимательской или профессиональной деятельностью. ## Право на отзыв Вы вправе отказаться от этого договора в течение 14 дней без объяснения причин. Срок отзыва составляет 14 дней со дня заключения договора. Чтобы воспользоваться правом, сообщите нам о своём решении однозначным заявлением. Быстрее всего это сделать кнопкой ниже; письмо на почту тоже подойдёт. Для соблюдения срока достаточно отправить уведомление до его истечения. ## Куда направлять algopath.pro - Elman Huseynov c/o Online-Impressum #9942 Europaring 90 53757 Sankt Augustin, Deutschland Эл. почта: contact@elman.group ## Последствия отзыва При отзыве мы возвращаем все полученные от вас платежи без неоправданной задержки и не позднее 14 дней со дня получения уведомления. Возврат делается тем же способом оплаты, каким вы платили, если прямо не согласовано иное; комиссия за это с вас не берётся. Платежи проходят через Stripe как продавца, поэтому возврат делает Stripe. ## Досрочное прекращение права Для цифрового контента право на отзыв прекращается досрочно, если вы прямо согласились на начало исполнения до истечения срока отзыва и подтвердили, что тем самым теряете это право. Страница оплаты спрашивает об обоих пунктах до платежа и сохраняет ваш ответ с отметкой времени. Без такого согласия 14-дневное право действует полностью. ## Образец заявления об отзыве «Elman Huseynov, contact@elman.group: настоящим я отзываю заключённый мной договор о покупке доступа к курсу AlgoPath. Дата заказа: … . Имя потребителя: … . Адрес потребителя: … . Дата: … .» Отзыв в два шага, без входа в аккаунт. После подтверждения вы получите письмо с точной датой и временем подачи. [Vertrag widerrufen](/ru/widerruf) --- title: "Аналоги LeetCode: десять площадок рядом" url: https://algopath.pro/ru/leetcode-alternatives language: ru updated: 2026-10-22 --- # Аналоги LeetCode: десять площадок рядом Коротко. Оставайся на LeetCode, если нужен объём и задачи по конкретным компаниям. Бери NeetCode, если нужен готовый список с видео на каждую задачу. Нужны условия на русском и контесты? Иди на CodeRun от Яндекса или на Codeforces. Решил сотню задач и всё ещё не пишешь бинарный поиск по памяти? Смотри AlgoPath. Аналоги LeetCode делятся на три вида: банки задач, курсы с видеоразбором и тренажёры на память рук. Ниже семь площадок рядом, с ценой и с тем, что ты на каждой реально делаешь, а после них три русскоязычных архива задач. *Обновлено 15 октября 2026.* ## Аналоги LeetCode рядом | Платформа | Что ты реально делаешь | Кому подходит | Цена | | --- | --- | --- | --- | | [LeetCode](https://leetcode.com) | Решаешь задачи из банка на тысячи штук | Объём, контесты, задачи по компаниям | Есть бесплатный уровень, Premium $35 в месяц или $159 в год | | [NeetCode](https://neetcode.io) | Идёшь по готовому списку, на каждую задачу видео | Тем, кому надо показать ход решения | Видео и списки бесплатно, Pro $119 за год или $297 разово | | **AlgoPath** | Пишешь каждый алгоритм сам, потом встраиваешь в рабочую систему | Тем, кто хочет писать код по памяти | Первые 33 шага бесплатно, дальше один платёж | | [AlgoExpert](https://www.algoexpert.io) | Отобранные задачи с видеоразбором | Тем, кто хочет готовый маршрут | $119 за год | | [Codewars](https://www.codewars.com) | Ката от сообщества, по уровням | Ежедневная разминка на своём языке | Бесплатно | | [HackerRank](https://www.hackerrank.com) | Треки плюс тест, которым многие компании отсеивают | Привыкнуть к формату отсева | Есть бесплатный уровень | | [Exercism](https://exercism.org) | Упражнения с живым ментором | Изучение языка, а не подготовка к собесу | Бесплатно | ## Что из этого на русском и что бесплатно Из семи площадок в таблице полностью на русском только AlgoPath: и сайт, и все 150 шагов курса. Остальные шесть по умолчанию работают на английском, включая условия задач. Бесплатно решать можно на Codewars и Exercism. Платные дополнения там (Codewars Red за $5 в месяц, Exercism Insiders за пожертвование) дают статистику и убирают рекламу, задачи за ними не спрятаны. У LeetCode и HackerRank есть бесплатный уровень. На LeetCode за оплатой лежат разбивка по компаниям и задачи с пометкой Premium, на HackerRank дополнительные пробные собеседования с ИИ. У AlgoPath бесплатны первые 33 шага. Английский на собеседовании всё равно понадобится. Разбираться в новом алгоритме проще на родном языке, а перенести разобранное на английскую формулировку легче, чем наоборот. Сорок слов, которых хватает для английских условий, собраны в статье [LeetCode на русском](/ru/blog/leetcode-na-russkom). ## Где решать задачи на русском Три самых известных русскоязычных площадки. Это банки задач и контесты, курса по паттернам там нет. | Платформа | Что ты реально делаешь | Кому подходит | Цена | | --- | --- | --- | --- | | [CodeRun](https://coderun.yandex.ru) | Решаешь задачи на тренажёре Яндекса, код проверяется автоматически, чужие разборы открыты | Подготовка к техническому собесу | Бесплатно | | [Codeforces](https://codeforces.com) | Пишешь контесты с рейтингом и решаешь задачи из архива | Скорость под таймер, олимпиадные задачи | Бесплатно | | [Timus Online Judge](https://acm.timus.ru) | Решаешь задачи из архива уральских олимпиад и ICPC | Студенты и олимпиадники | Бесплатно | Условия на всех трёх есть на русском. Codeforces и Timus переключаются и на английский. Какую площадку брать под собеседование, контесты или изучение языка, разобрано в статье [где решать задачи по программированию](/ru/blog/gde-reshat-zadachi-po-programmirovaniyu). Там ты набиваешь объём и скорость. Шаги AlgoPath учат узнавать паттерн в условии и писать сам алгоритм по памяти, поэтому одно хорошо идёт вместе с другим. ## Где LeetCode всё ещё лучше Задач там больше, чем где-либо. Под каждой обсуждение. Premium даёт разбивку по компаниям. Если знаешь, куда идёшь на собес, заменить это тяжело. Каждую неделю контест. Это настоящая практика под таймер. Собес через две недели и паттерны ты уже знаешь? Тогда нужен объём. Иди туда. ## Что AlgoPath делает иначе - Один шаг это один алгоритм, и пишешь его ты. 150 шагов, в строгом порядке. - Шесть языков прямо в браузере: JavaScript, TypeScript, Python, PHP, Java и C#. Ставить ничего не надо. - Каждый шаг заканчивается системой, где алгоритм реально работает. Рейт-лимитер. LRU-кеш. Git bisect. - Тренажёр на то, что собес проверяет первым: прочитать задачу и назвать [паттерн](/ru/patterns). - Всё на двух языках, английском и русском. ## Что выбрать, зависит от того, что ломается Проведи один мок-собес и посмотри, где сыпется. - Читаешь условие и зависаешь. Это распознавание. Гоняй паттерны. - Подход называешь верно, а код выходит кривой. Это память рук. Пиши алгоритмы руками на [тренажёре алгоритмов](/ru/blog/trenazher-algoritmov). - И то и другое нормально, но медленно. Это объём. Решай задачи пачками. ## Сравнения один на один В каждом сказано, где другой продукт сильнее. Цены взяты со страницы самого сервиса. - [AlgoPath или NeetCode](/ru/algopath-vs-neetcode) - [Стоит ли платить за AlgoExpert](/ru/algopath-vs-algoexpert) - [Стоит ли платить за Structy](/ru/algopath-vs-structy) - [Стоит ли платить за AlgoMonster](/ru/algopath-vs-algomonster) - [Стоит ли покупать LeetCode Premium](/ru/is-leetcode-premium-worth-it) ## Частые вопросы ### Есть ли аналог LeetCode от Яндекса? Да, это CodeRun. Условия на русском, код проверяется автоматически, решения других участников открыты. Решать бесплатно. ### Какие есть российские аналоги LeetCode? Самые известные три: CodeRun от Яндекса, Codeforces и Timus Online Judge из Уральского федерального университета. Все бесплатные, условия на русском. Это банки задач и контесты, теории по темам там нет. ### Есть ли на LeetCode русский язык? Нет. Интерфейс, условия задач и обсуждения на английском, переключателя на русский нет. Отдельная версия сайта есть только на китайском, leetcode.cn. ### Какие аналоги LeetCode бесплатные? Полностью бесплатны Codewars, Exercism, CodeRun, Codeforces и Timus. У LeetCode и HackerRank есть бесплатный уровень, его хватает на большую часть задач. У NeetCode бесплатны видео и списки задач, у AlgoPath первые 33 шага вместе с раннером и тестами. ### Что лучше, LeetCode или Codewars? Для подготовки к собеседованию LeetCode: задачи того же формата, что на интервью, и есть разбивка по компаниям. Codewars удобен для ежедневной разминки на своём языке. Задачи там короче и разложены по рангам от 8 kyu, самого простого, до 1 kyu. ## Проверь до того, как решать [Тест на уровень](/ru/placement) это 15 вопросов, аккаунт не нужен. Он скажет, с какой темы начинать. Первые 33 шага бесплатно, вместе с раннером и тестами. В них есть [двумерные префиксные суммы](/ru/steps/prefix-2d) и [бинарный поиск по ответу](/ru/steps/binary-search-on-answer). [Полный доступ](/ru/pricing) это один платёж, без подписки. Ещё не решил? Сначала прочитай, [как готовиться](/ru/best-way-to-practice-dsa). --- title: "NeetCode: отзыв и чего в нём не хватает" url: https://algopath.pro/ru/algopath-vs-neetcode language: ru updated: 2026-10-16 --- # NeetCode: отзыв и чего в нём не хватает Коротко. NeetCode показывает, как приходят к решению. AlgoPath заставляет написать алгоритм до того, как ты увидишь чужой. Хорошо учишься по видео? Начни с NeetCode. Видео перестало двигать вперёд? Тогда сюда. Видео NeetCode бесплатны, у AlgoPath бесплатны первые 33 шага, так что попробовать можно оба без оплаты. *Обновлено 15 октября 2026.* ## Рядом | | NeetCode | AlgoPath | | --- | --- | --- | | Единица работы | Одна задача с собеса | Один алгоритм | | Что получается на выходе | Решение этой задачи | Алгоритм, а потом система, которая его использует | | Объяснения | Видео на каждую задачу | Текстовая теория, потом лесенка подсказок, которая подталкивает, а не выдаёт ответ | | Языки | Решения на нескольких | Шесть, прямо в браузере: JS, TS, Python, PHP, Java, C# | | Где выполняется твой код | В твоём редакторе | В браузере, вместе с тестами | | Порядок | Готовый список, берёшь что хочешь | 150 шагов в жёсткой цепочке | | Что бесплатно | Видео и списки NeetCode 150 и 250 | Первые 33 шага | | Что платно | Pro: $119 за год или $297 разово | Один платёж, без продлений | ## Где NeetCode лучше Видео. Их сотни, они бесплатные, и объясняют понятно. NeetCode 150 это хороший список. Тебе дают порядок и проговаривают каждый ответ. [AlgoExpert](/ru/algopath-vs-algoexpert) продаёт такой же список, 200 задач с видео к каждой, по годовой цене. У AlgoPath нет видеотеки такого размера, и мы это не скрываем. ## Где лучше AlgoPath Смотреть решение и выдавать решение это два разных навыка. Здесь качают второй. Каждый шаг даёт файл с сигнатурой, TODO и тестами. Тело функции пишешь ты. Тесты красные, пока не заработает. Дальше та же функция идёт в маленький продакшен-кейс. Видишь алгоритм за работой. Ещё есть тренажёр на [распознавание паттернов](/ru/patterns), 52 штуки. И раунд под таймер, вслух. ## Как совмещать Так делают многие, и это работает. Сначала собери алгоритм здесь. Потом посмотри видео NeetCode на задачу, где он нужен. Увидишь чужой ход мысли. Порядок важен. Сначала видео, и ты запомнишь решение. Сначала руки, и у тебя останется алгоритм. ## Итог - Только начинаешь и нужен маршрут: NeetCode. - Теорию знаешь, нужен объём и задачи по компаниям: [LeetCode Premium](/ru/is-leetcode-premium-worth-it). - Чужое решение понимаешь, своё выдать не можешь: AlgoPath. - Лучше всего учишься по анимациям и хочешь сообщество: [Structy](/ru/algopath-vs-structy). ## Частые вопросы ### Что такое NeetCode? Сайт для подготовки к алгоритмическому собеседованию. Задачи LeetCode там собраны в списки NeetCode 150 и NeetCode 250, к каждой записан видеоразбор. Видео на английском. ### NeetCode бесплатный? Большей частью да. Видеоразборы, списки NeetCode 150 и 250 и роадмап по темам открыты без оплаты. Платный только Pro. ### Сколько стоит NeetCode Pro и что в нём? $119 за год или $297 одним платежом, по странице neetcode.io/pro в октябре 2026. Pro добавляет видеокурсы и большой банк задач со своим раннером. Списки и видео остаются бесплатными и без него. ### Хватит ли NeetCode 150 для собеседования? По темам да: 150 задач в 18 разделах, от массивов и хешей до динамического программирования. Хватает, если через неделю ты пишешь решение заново в пустом файле. Если ответ узнаётся только после видео, пока не хватает. ### С чего начать: NeetCode или LeetCode? С NeetCode, если нужен порядок: список говорит, что решать дальше, а задачи в нём с LeetCode. Сам LeetCode пригодится позже, для объёма и задач по компаниям. ## Проверь [Тест на уровень](/ru/placement) это 15 вопросов, аккаунт не нужен. Первые 33 шага бесплатно, среди них [два указателя](/ru/steps/two-pointers-opposite-ends) и [скользящее окно со счётчиком](/ru/steps/sliding-window-hash-counter). [Полный доступ](/ru/pricing) это один платёж. Выбираешь не только между этими двумя? Вот [разбор альтернатив](/ru/leetcode-alternatives). --- title: "Как правильно учить алгоритмы" url: https://algopath.pro/ru/best-way-to-practice-dsa language: ru updated: 2026-10-16 --- # Как правильно учить алгоритмы Коротко. Качай три вещи по отдельности. Писать каждый алгоритм по памяти. Называть паттерн незнакомой задачи. Делать и то и другое под таймер, вслух, за 35 минут. Большинство делает только третье и удивляется, почему легче не становится. Банк задач тренирует скорость, а память рук тренирует только пустой файл. *Обновлено 15 октября 2026.* ## Три навыка, которые проверяет собес 1. **Память рук.** Написать бинарный поиск, кучу, систему непересекающихся множеств, ничего не открывая. Не получается? Новые задачи это не починят. Почему восстановить полезнее, чем перечитать, разобрано в статье [активное вспоминание](/ru/blog/aktivnoe-vspominanie). 2. **Распознавание.** Прочитать 200 слов условия и за минуту назвать паттерн. Здесь сыпется большинство. 3. **Скорость под давлением.** Первое и второе, вслух, за 35 минут. ## Что тренирует каждый способ | Способ | Тренирует | Не трогает | | --- | --- | --- | | Гонять банк задач | Скорость, насмотренность | Память рук. Открыл разбор и пошёл дальше. | | Смотреть видеоразборы | Немного распознавание | Память рук. Смотреть это не писать. | | Писать алгоритмы с чистого листа | Память рук | Скорость и выбор нужного алгоритма | | Карточки со сложностями | Факты | Всё, что делают руки | | Мок-собесы | Скорость, речь | Ничего нового. Это замер, а не обучение. | ## Неделя, которая работает - Понедельник, вторник, среда: по одному алгоритму в день. С чистого файла, пока тесты не позеленеют. - Четверг: пять незнакомых задач. Называешь паттерн до первой строки кода. Кодить пока нельзя. - Пятница: две задачи за 35 минут, вслух. - Выходные: переписать тот алгоритм, который в понедельник вышел криво. Четыре часа в неделю лучше одного марафона в воскресенье. Память рук уходит по расписанию, трогай её чаще. ## Ошибка, которая съедает больше всего времени Открыть разбор на десятой минуте. Дай себе 25 минут. Потом бери подсказку, а не ответ. Подсказка, которая понадобилась, показывает дыру, про которую ты теперь знаешь. ## Как понять, что идёт - Паттерн называешь ещё до конца условия. - Каркас пишешь, не вспоминая границы цикла. - Можешь объяснить, почему этот алгоритм, а не соседний. ## Как это устроено здесь AlgoPath собран ровно в таком порядке. 150 шагов, на каждом один алгоритм, пишешь его ты. Тесты гоняются в браузере, на шести языках. Первые 33 шага бесплатны, начиная с урока [что такое алгоритм](/ru/steps/what-is-an-algorithm). [Big-O](/ru/steps/big-o-and-constraints) появляется на шаге 9. Дальше тренажёр на распознавание, 52 [паттерна](/ru/patterns). И раунд под таймер на скорость. Чем тренажёр отличается от банка задач, разобрано в статье [тренажёр алгоритмов](/ru/blog/trenazher-algoritmov). Начни с [теста на уровень](/ru/placement). 15 вопросов, без аккаунта, и он скажет, какую тему открыть первой. Если выбираешь между платформами, есть [разбор альтернатив](/ru/leetcode-alternatives).