├── .gitignore ├── manuscript ├── 005_binary_004_hints.md ├── 005_binary_002_convex.md ├── resources │ ├── intro │ │ ├── vscode_01.png │ │ ├── vscode_02.png │ │ ├── vscode_03.png │ │ ├── vscode_04.png │ │ ├── vscode_05.png │ │ └── vscode_06.png │ ├── trees │ │ ├── kdistance.png │ │ ├── serialize.png │ │ ├── bst_example.png │ │ ├── bst_reorder.png │ │ ├── sum_structure.png │ │ ├── well_behaved.png │ │ ├── bst_partitions.png │ │ ├── bst_unballanced.png │ │ ├── paths_in_trees.png │ │ ├── in_order_traversal.png │ │ ├── sum_of_distances.png │ │ ├── kdistance_distances.png │ │ ├── post_order_traversal.png │ │ ├── pre_order_traversal.png │ │ ├── rank_order_traversal.png │ │ └── paths_crossing_a_node.png │ ├── binary_search │ │ ├── sonar.png │ │ ├── tree_search.png │ │ └── sonar_binary_search.png │ ├── traversal │ │ ├── maze_base.png │ │ ├── maze_bfs.png │ │ ├── maze_dfs.png │ │ ├── bus_routes.png │ │ ├── backtracking.png │ │ ├── locked_rooms.png │ │ ├── sudoku_solver.png │ │ ├── counting_islands.png │ │ ├── locked_rooms_dfs.png │ │ ├── sudoku_constraints.png │ │ └── valid_parentheses.png │ └── linked_list │ │ ├── list_loop_01.png │ │ ├── list_reverse_01.png │ │ ├── list_loop_detect_01.png │ │ ├── list_loop_detect_02.png │ │ ├── list_loop_detect_03.png │ │ ├── list_loop_detect_04.png │ │ ├── list_loop_start_01.png │ │ ├── list_loop_start_02.png │ │ ├── list_merge_k_lists_01.png │ │ ├── list_reverse_k_groups_01.png │ │ ├── list_remove_kth_element_01.png │ │ ├── list_reverse_kgroup_solution_01.png │ │ ├── list_reverse_kgroup_solution_02.png │ │ ├── list_reverse_kgroup_solution_03.png │ │ ├── list_reverse_kgroup_solution_04.png │ │ └── list_reverse_kgroup_solution_05.png ├── Book.txt ├── 005_binary_003_problems.md ├── 002_lists_004_hints.md ├── 003_traversal_004_hints.md ├── 004_trees_006_hints.md ├── 001_001_preface.md ├── 005_binary_005_solutions.md ├── 001_002_introduction.md ├── 002_lists_003_problems.md ├── 002_lists_002_simple.md ├── 003_traversal_003_problems.md ├── 004_trees_003_bst.md ├── 004_trees_005_problems.md ├── 004_trees_004_paths.md ├── 005_binary_001_monotonic.md ├── 004_trees_001_theory.md ├── 003_traversal_001_theory.md ├── 002_lists_001_theory.md ├── 004_trees_002_traversal.md ├── 002_lists_005_solutions.md ├── 003_traversal_002_variants.md ├── 003_traversal_005_solutions.md └── 004_trees_007_solutions.md ├── book_cover.png ├── .vscode └── settings.json ├── README.md └── LICENSE.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /manuscript/005_binary_004_hints.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Hints -------------------------------------------------------------------------------- /book_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/book_cover.png -------------------------------------------------------------------------------- /manuscript/005_binary_002_convex.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Binary search on convex and concave sequences 3 | 4 | -------------------------------------------------------------------------------- /manuscript/resources/intro/vscode_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/intro/vscode_01.png -------------------------------------------------------------------------------- /manuscript/resources/intro/vscode_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/intro/vscode_02.png -------------------------------------------------------------------------------- /manuscript/resources/intro/vscode_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/intro/vscode_03.png -------------------------------------------------------------------------------- /manuscript/resources/intro/vscode_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/intro/vscode_04.png -------------------------------------------------------------------------------- /manuscript/resources/intro/vscode_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/intro/vscode_05.png -------------------------------------------------------------------------------- /manuscript/resources/intro/vscode_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/intro/vscode_06.png -------------------------------------------------------------------------------- /manuscript/resources/trees/kdistance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/kdistance.png -------------------------------------------------------------------------------- /manuscript/resources/trees/serialize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/serialize.png -------------------------------------------------------------------------------- /manuscript/resources/binary_search/sonar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/binary_search/sonar.png -------------------------------------------------------------------------------- /manuscript/resources/traversal/maze_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/traversal/maze_base.png -------------------------------------------------------------------------------- /manuscript/resources/traversal/maze_bfs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/traversal/maze_bfs.png -------------------------------------------------------------------------------- /manuscript/resources/traversal/maze_dfs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/traversal/maze_dfs.png -------------------------------------------------------------------------------- /manuscript/resources/trees/bst_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/bst_example.png -------------------------------------------------------------------------------- /manuscript/resources/trees/bst_reorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/bst_reorder.png -------------------------------------------------------------------------------- /manuscript/resources/trees/sum_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/sum_structure.png -------------------------------------------------------------------------------- /manuscript/resources/trees/well_behaved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/well_behaved.png -------------------------------------------------------------------------------- /manuscript/resources/traversal/bus_routes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/traversal/bus_routes.png -------------------------------------------------------------------------------- /manuscript/resources/trees/bst_partitions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/bst_partitions.png -------------------------------------------------------------------------------- /manuscript/resources/trees/bst_unballanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/bst_unballanced.png -------------------------------------------------------------------------------- /manuscript/resources/trees/paths_in_trees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/paths_in_trees.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_loop_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_loop_01.png -------------------------------------------------------------------------------- /manuscript/resources/traversal/backtracking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/traversal/backtracking.png -------------------------------------------------------------------------------- /manuscript/resources/traversal/locked_rooms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/traversal/locked_rooms.png -------------------------------------------------------------------------------- /manuscript/resources/traversal/sudoku_solver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/traversal/sudoku_solver.png -------------------------------------------------------------------------------- /manuscript/resources/trees/in_order_traversal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/in_order_traversal.png -------------------------------------------------------------------------------- /manuscript/resources/trees/sum_of_distances.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/sum_of_distances.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "grammarly.selectors": [ 3 | { 4 | "language": "markdown", 5 | "scheme": "file" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /manuscript/resources/binary_search/tree_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/binary_search/tree_search.png -------------------------------------------------------------------------------- /manuscript/resources/traversal/counting_islands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/traversal/counting_islands.png -------------------------------------------------------------------------------- /manuscript/resources/traversal/locked_rooms_dfs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/traversal/locked_rooms_dfs.png -------------------------------------------------------------------------------- /manuscript/resources/trees/kdistance_distances.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/kdistance_distances.png -------------------------------------------------------------------------------- /manuscript/resources/trees/post_order_traversal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/post_order_traversal.png -------------------------------------------------------------------------------- /manuscript/resources/trees/pre_order_traversal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/pre_order_traversal.png -------------------------------------------------------------------------------- /manuscript/resources/trees/rank_order_traversal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/rank_order_traversal.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_reverse_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_reverse_01.png -------------------------------------------------------------------------------- /manuscript/resources/traversal/sudoku_constraints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/traversal/sudoku_constraints.png -------------------------------------------------------------------------------- /manuscript/resources/traversal/valid_parentheses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/traversal/valid_parentheses.png -------------------------------------------------------------------------------- /manuscript/resources/trees/paths_crossing_a_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/trees/paths_crossing_a_node.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_loop_detect_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_loop_detect_01.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_loop_detect_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_loop_detect_02.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_loop_detect_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_loop_detect_03.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_loop_detect_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_loop_detect_04.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_loop_start_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_loop_start_01.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_loop_start_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_loop_start_02.png -------------------------------------------------------------------------------- /manuscript/resources/binary_search/sonar_binary_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/binary_search/sonar_binary_search.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_merge_k_lists_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_merge_k_lists_01.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_reverse_k_groups_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_reverse_k_groups_01.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_remove_kth_element_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_remove_kth_element_01.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_reverse_kgroup_solution_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_reverse_kgroup_solution_01.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_reverse_kgroup_solution_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_reverse_kgroup_solution_02.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_reverse_kgroup_solution_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_reverse_kgroup_solution_03.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_reverse_kgroup_solution_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_reverse_kgroup_solution_04.png -------------------------------------------------------------------------------- /manuscript/resources/linked_list/list_reverse_kgroup_solution_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HappyCerberus/cpp-coding-interview/HEAD/manuscript/resources/linked_list/list_reverse_kgroup_solution_05.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Surviving the C++ Coding Interview 2 | 3 | This repository contains the text source for the [Surviving the C++ Coding Interview](https://leanpub.com/cpp-coding-interview) book. 4 | 5 | [![Surviving the C++ Coding Interview](book_cover.png)](https://leanpub.com/cpp-coding-interview) 6 | 7 | This book is part of my Creative Commons series (under the CC-BY-NC-SA 4.0 license); the Community Edition is identical to the Commercial Edition, and 100% of my royalty goes to Electronic Frontier Foundation. 8 | 9 | You can grab the Community Edition [by following this link](https://leanpub.com/cpp-coding-interview/signup). -------------------------------------------------------------------------------- /manuscript/Book.txt: -------------------------------------------------------------------------------- 1 | 001_001_preface.md 2 | 001_002_introduction.md 3 | 002_lists_001_theory.md 4 | 002_lists_002_simple.md 5 | 002_lists_003_problems.md 6 | 002_lists_004_hints.md 7 | 002_lists_005_solutions.md 8 | 003_traversal_001_theory.md 9 | 003_traversal_002_variants.md 10 | 003_traversal_003_problems.md 11 | 003_traversal_004_hints.md 12 | 003_traversal_005_solutions.md 13 | 004_trees_001_theory.md 14 | 004_trees_002_traversal.md 15 | 004_trees_003_bst.md 16 | 004_trees_004_paths.md 17 | 004_trees_005_problems.md 18 | 004_trees_006_hints.md 19 | 004_trees_007_solutions.md 20 | 005_binary_001_monotonic.md 21 | 005_binary_002_convex.md 22 | 005_binary_003_problems.md 23 | 005_binary_004_hints.md 24 | 005_binary_005_solutions.md -------------------------------------------------------------------------------- /manuscript/005_binary_003_problems.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Canonical problems 3 | 4 | The typical interview questions that you will encounter will have you exercise the standard binary search algorithms we discussed in the theory section. 5 | 6 | Instead of rehashing the basics, the following problems explore the more niche uses of binary search. 7 | 8 | ### Sonar 9 | 10 | You are given a rectangular map and access to a sonar system with a simple "ping" method. 11 | 12 | Determine the number of ships on the map as quickly as possible. The ping method takes a rectangular area as an argument and returns presence information (whether any ships are in the given area). 13 | 14 | {width: "40%"} 15 | ![Example of four sonar queries. Queries returning true in green, queries returning false in red.](binary_search/sonar.png) 16 | 17 | The area specified for both the input and the ping method is border-inclusive. 18 | 19 | {class: information} 20 | B> The scaffolding for this problem is located at `binary_search/sonar`. Your goal is to make the following commands pass without any errors: `bazel test //binary_search/sonar/...`, `bazel test --config=addrsan //binary_search/sonar/...`, `bazel test --config=ubsan //binary_search/sonar/...`. -------------------------------------------------------------------------------- /manuscript/002_lists_004_hints.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Hints 3 | 4 | ### Reverse k-groups in a list 5 | 6 | 1. You will need a function to reverse a list. 7 | 2. When you reverse k elements, what needs to re-connect to what? Drawing a picture may help. 8 | 3. When we reverse a group of k elements, what was previously the first element is now the last element of the group. 9 | 10 | ### Merge a list of sorted lists 11 | 12 | 1. There are two ways to achieve *log* scaling. 13 | 2. If you can decrease the number of unmerged lists by a factor of two in each step, you will get *log* scaling. 14 | 3. Picking the lowest element from a sorted range is *log(n)*. 15 | 16 | ### Remove the kth element from the end of a singly-linked list 17 | 18 | 1. How can you check that an element is the kth element from the end of the list? 19 | 2. How can you re-use the information you calculated to check the next element? 20 | 21 | ### Find a loop in a linked list 22 | 23 | 1. What happens when there is no loop? 24 | 2. If we iterate over the list and there is a loop, we get stuck; how could we detect this situation? 25 | 3. Can we use two pointers? 26 | 4. If we use one slow (moving by one) and fast (moving by two) pointer, they will meet up if there is a loop. -------------------------------------------------------------------------------- /manuscript/003_traversal_004_hints.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Hints 3 | 4 | ### Locked rooms 5 | 6 | 1. Think about the keys. 7 | 2. The question we need to answer is whether we can collect all keys. 8 | 3. We do not need to find the shortest route; therefore, a depth-first search will be good enough. 9 | 10 | ### Bus routes 11 | 12 | 1. Don't think in terms of bus stops. 13 | 2. You will need to pre-compute something. 14 | 3. Pre-computing a connection mapping, which other buses we can switch to, will allow you to traverse over the buses. 15 | 4. We need the shortest path and must use a breadth-first search. 16 | 17 | ### Counting islands 18 | 19 | 1. If we traverse a potential island, we will visit all its and neighbouring spaces. 20 | 2. What type of space will we not encounter if the landmass is an island? 21 | 22 | ### All valid parentheses 23 | 24 | 1. What are the properties that hold true for a valid parentheses sequence? 25 | 2. We only have n pairs of parentheses. This leads to one constraint. 26 | 3. We can only add a right parenthesis under specific circumstances. This leads to one constraint. 27 | 4. We need the backtracking algorithm to find all paths that satisfy the above constraints. 28 | 29 | ### Sudoku solver 30 | 31 | 1. We have walked through the solution for a very similar problem. 32 | 2. Try modifying the solution for N-Queens. 33 | 3. What constraints can we propagate to improve the solving performance? -------------------------------------------------------------------------------- /manuscript/004_trees_006_hints.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Hints 3 | 4 | ### Serialise and de-serialise n-ary tree 5 | 6 | 1. We have covered an approach for (de)serializing a binary tree. 7 | 2. Use pre-order traversal with a format that can represent the list of children. 8 | 3. You will need to store the number of children or use a terminal value. 9 | 10 | ### Find all nodes of distance *k* in a binary tree 11 | 12 | 1. Can you calculate the value for a child if you know the distance for the parent node? 13 | 2. Consider whether the child lies on the path towards the target or not. 14 | 3. You will need a two-step process: finding the path to the target from the root and then calculating the distances. 15 | 16 | ### Sum of distances to all nodes 17 | 18 | 1. Can you calculate the value for the root node? 19 | 2. You can use post-order traversal to calculate the sum of distance for the root node from the children trees. 20 | 3. What would happen if we removed one edge, calculated the values for the two roots, and then recombined? 21 | 4. You should be able to derive a straightforward formula for calculating the sum of distances for a child if you know the value for the parent node. 22 | 5. If you consider the values, a straightforward formula for calculating the sum of distances for a child from the parent node will pop out. 23 | 6. Once you have the formula, a pre-order traversal will allow you to fill in the missing values. 24 | 25 | ### Well-behaved paths in a tree 26 | 27 | 1. You will want to iterate over edges instead of nodes. 28 | 2. Suppose we connect two sub-trees with an edge, and the maximum value in both sub-trees is the same. In that case, we can trivially calculate the number of paths this connection generates if you know the number of instances of the maximum value in both trees. 29 | 3. You will need the union-find algorithm for efficient lookup. 30 | 31 | ### Number of reorders of a serialized BST 32 | 33 | 1. What is the role of the first value in the serialized tree? 34 | 2. We end up with two partitions; what operation can we do that doesn't affect the value of these partitions? 35 | 3. You will need to pre-calculate the Pascal triangle. -------------------------------------------------------------------------------- /manuscript/001_001_preface.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | # Preface 3 | 4 | Welcome, you are reading the Surviving the C++ coding interview book. I conceived this book's idea during the mass layoffs of 2022/2023. 5 | 6 | Most companies still insist on using coding puzzles during interviews, and while for some, this is only a convenient scaffolding, for many, it remains the primary filter for candidates. Therefore training for this part of the interview remains a necessity. 7 | 8 | This book aims to guide you through the different types of problems you can come across while also focusing on information that will remain relevant past the interview process. 9 | 10 | After finishing this book, you should be able to recognize the solution patterns when presented with a problem and transform those patterns into an implementation. 11 | 12 | As with all my books, this book focuses on practical information. The text is interspersed with commented examples, and the book comes with a companion repository that contains a comprehensive test suite. You are encouraged to attempt to solve each of the presented problems yourself and only then compare it with the commented solution. 13 | 14 | ## The commercial edition vs. community edition 15 | 16 | This book is part of my Creative Common series (under the CC-BY-NC-SA 4.0 license), and as such, the commercial edition doesn't differ from the community edition. 17 | 18 | On top of that, 100% of my royalties from the purchases of this book go to the Electronic Frontier Foundation. 19 | 20 | ## The author 21 | 22 | I am Šimon Tóth, the sole author of this book. My primary qualification is 20 years of C++ experience, with C++ being my primary language in a commercial setting for approximately 15 of those years. 23 | 24 | My background is in HPC, spanning academia, big tech, and startup environments. 25 | 26 | I have architected, built, and operated systems of all scales, from single-machine hardware-supported high-availability to planet-scale services. 27 | 28 | My passion has always been teaching and mentoring junior engineers throughout my career, which is why you are now reading this book. 29 | 30 | For more about me, check out my [LinkedIn profile](https://www.linkedin.com/in/simontoth/). -------------------------------------------------------------------------------- /manuscript/005_binary_005_solutions.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Solutions 3 | 4 | ### Sonar 5 | 6 | Let's start with a simplified version of the problem. What if we were looking for a single ship on a 1D map? 7 | 8 | Consider that when we binary search in an array, we use a predicate to decide which half of the range we should recurse in. Instead of checking against a value, we can reformulate the question to "Is the value in the left half of the range?" If we get true, we know we need to recurse in the left side of the range; if we get false, we recurse in the right side of the range. 9 | 10 | {width: "40%"} 11 | ![Example of a binary search using a "presence" predicate when searching for value 5 (pivots underlined).](binary_search/sonar_binary_search.png) 12 | 13 | This means we can use the sonar ping function in place of a predicate. However, we still need to deal with the fact that we are looking for potentially many ships on a 2D map. 14 | 15 | The many ships situation means we can potentially get true for both the left and right half of the range, so instead of recursing into one half, we may need to recurse into both the left and right halves. 16 | 17 | The 2D map increases the dimensionality of our binary search. Instead of dividing a range into two parts, a pivot now divides the map into four quadrants. 18 | 19 | Putting this together we end up with recursive quad-search: 20 | 21 | {caption: "Solution for the Sonar problem."} 22 | ```cpp 23 | size_t count_ships(const Sonar& sonar, Area area) { 24 | // for non-square area we can end up overflowing 25 | // the limit in one of the dimensions 26 | if (area.bottom > area.top) return 0; 27 | if (area.left > area.right) return 0; 28 | 29 | // nothing in the area 30 | if (!sonar.ping(area)) return 0; 31 | 32 | // there is something and this is area of size one 33 | if (area.bottom == area.top && area.left == area.right) 34 | return 1; 35 | 36 | // otherwise calculate the four quadrants and recurse 37 | size_t mid_x = std::midpoint(area.left,area.right); 38 | size_t mid_y = std::midpoint(area.bottom,area.top); 39 | 40 | size_t count = 0; 41 | count += count_ships(sonar, 42 | {.bottom = area.bottom, .top = mid_y, 43 | .left = area.left, .right = mid_x}); 44 | count += count_ships(sonar, 45 | {.bottom = mid_y + 1, .top = area.top, 46 | .left = area.left, .right = mid_x}); 47 | count += count_ships(sonar, 48 | {.bottom = area.bottom, .top = mid_y, 49 | .left = mid_x+1, .right = area.right}); 50 | count += count_ships(sonar, 51 | {.bottom = mid_y + 1, .top = area.top, 52 | .left = mid_x+1, .right = area.right}); 53 | return count; 54 | } 55 | ``` -------------------------------------------------------------------------------- /manuscript/001_002_introduction.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | # Introduction 3 | 4 | ## Book structure 5 | 6 | I believe in interleaving theory and practical training, and I have structured this book to facilitate and enrich this workflow. Each chapter adheres to a consistent structure to ensure a steady progression. 7 | 8 | We start with an introduction that covers relevant C++ background as necessary. Then, we move on to essential patterns and simple operations. Each chapter concludes with carefully selected problems, complemented by solutions and commentary. 9 | 10 | While the chapters are sequential, with each building on the foundations of the previous, the book doesn't restrict you to strict reading order. Instead, it comes with a comprehensive index. You can always refer to the index to look up more details if you encounter an unfamiliar concept or algorithm. 11 | 12 | ## Companion repository 13 | 14 | This book has a [companion repository](https://github.com/HappyCerberus/cpp-coding-interview-companion) with a test suite and scaffolding for each problem. 15 | 16 | The repository is set up with a DevContainer configuration. It allows for a seamless C++ development environment, equipped with the latest stable versions of GCC, GDB, and Clang when accessed through VS Code. All you need to take full advantage of this are [Visual Studio Code](https://code.visualstudio.com/download) and [Docker](https://www.docker.com/products/docker-desktop/). 17 | 18 | To get up and running, follow these steps: 19 | 20 | 1. Open Visual Studio Code, and select *View>Command Palette*. ![Open Command Palette](intro/vscode_01.png) 21 | 2. Write in `git clone` and select the *Git: Clone* action. ![Write git clone](intro/vscode_02.png) 22 | 3. Paste in the companion repository URL: [github.com/HappyCerberus/cpp-coding-interview-companion](https://github.com/HappyCerberus/cpp-coding-interview-companion) and confirm. ![Paste in the URL](intro/vscode_03.png) 23 | 4. Visual Studio Code will ask for a location and, once done, will ask to open the cloned repository. Confirm. ![Confirm open](intro/vscode_04.png) 24 | 5. Visual Studio Code will now ask whether you trust me. Confirm that you do. You can see all the relevant configuration inside the repository in the `.vscode` and `.devcontainer` directories. ![Confirm trust](intro/vscode_05.png) 25 | 6. Finally, Visual Studio Code will detect the devcontainer configuration and ask whether you want to re-open the project in the devcontainer. Confirm. After VSCode downloads the container, you will have a fully working C++ development environment with the latest GCC, Clang, GDB, and Bazel. ![Reopen in container](intro/vscode_06.png) 26 | 27 | ## Using this book 28 | 29 | The process you employ to solve the problems presented in this book is essential. Typically, in a coding interview, you vocalize your thoughts to allow for feedback and guidance. However, while using this book, that instant interaction isn't available. Should you find yourself stuck, consider this strategy: 30 | 31 | First, ensure you grasp the problem at hand. Then, try sketching it out or examining some examples on paper. 32 | 33 | Next, see if you can implement a simple brute-force solution as a starting point. From there, ask yourself what could be optimized. Is there repeated work? Can one solution inform another? 34 | 35 | If you're genuinely stumped, the hints section could offer valuable insight. -------------------------------------------------------------------------------- /manuscript/002_lists_003_problems.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Canonical problems 3 | 4 | Now that we've covered the basics, let's move on to real-world problems often seen in technical interviews. This next section will cover four linked list challenges: reversing k-groups in a list, merging a list of sorted lists, removing the kth element from the end, and finding a loop in a corrupted list. 5 | 6 | It's a step up from what we've done so far, but with the foundation you've built, you should be well-prepared to handle these tasks. Let's get started. 7 | 8 | ### Reverse k-groups in a list 9 | 10 | Our first challenge is all about diligence. Given a singly-linked list and a positive integer k, transpose the list so that each group of k elements is in reversed internal order. If k doesn't divide the number of elements without a remainder, the remainder of nodes should be left in their original order{i: "lists!reverse"}. 11 | 12 | ![Example of reversing groups of 3 elements.](linked_list/list_reverse_k_groups_01.png) 13 | 14 | You should be able to implement a version that operates in *O(n)* time and *O(1)* additional space, where n is the number of elements in the list. 15 | 16 | {class: information} 17 | B> The scaffolding for this problem is located at `lists/k_groups`. Your goal is to make the following commands pass without any errors: `bazel test //lists/k_groups/...`, `bazel test --config=addrsan //lists/k_groups/...`, `bazel test --config=ubsan //lists/k_groups/...`. 18 | 19 | ### Merge a list of sorted lists 20 | 21 | Given a list of sorted lists, return a merged sorted list{i: "divide and conquer"}{i: "always-sorted data structure"}. 22 | 23 | ![Example of merging three sorted lists.](linked_list/list_merge_k_lists_01.png) 24 | 25 | You should be able to implement a version that operates in *O(n\*log(k))* time and uses *O(k)* additional memory, where n is the total number of elements and k is the number of lists we are merging. 26 | 27 | {class: information} 28 | B> The scaffolding for this problem is located at `lists/merge`. Your goal is to make the following commands pass without any errors: `bazel test //lists/merge/...`, `bazel test --config=addrsan //lists/merge/...`, `bazel test --config=ubsan //lists/merge/...`. 29 | 30 | ### Remove the kth element from the end of a singly-linked list 31 | 32 | Given a singly-linked list and a positive integer k, remove the kth element (counted) from the end of the list{i: "two pointers!sliding window"}. 33 | 34 | ![Example of removing the 3rd element from the end of the list.](linked_list/list_remove_kth_element_01.png) 35 | 36 | You should be able to implement a version that operates in *O(n)* time and uses *O(1)* additional memory, where *n* is the number of elements in the list. 37 | 38 | {class: information} 39 | B> The scaffolding for this problem is located at `lists/end_of_list`. Your goal is to make the following commands pass without any errors: `bazel test //lists/end_of_list/...`, `bazel test --config=addrsan //lists/end_of_list/...`, `bazel test --config=ubsan //lists/end_of_list/...`. 40 | 41 | ### Find a loop in a linked list 42 | 43 | Given a potentially corrupted singly-linked list, determine whether it is corrupted, i.e., it forms a loop{i: "two pointers!slow and fast"}. 44 | 45 | ![Example of a corrupted list.](linked_list/list_loop_01.png) 46 | 47 | - Progression A: return the node that is the first node on the loop. 48 | - Progression B: fix the list. 49 | 50 | You should be able to implement a version that operates in *O(n)* and uses *O(1)* additional memory, where n is the number of elements in the list. 51 | 52 | {class: information} 53 | B> The scaffolding for this problem is located at `lists/loop` for the basic version and `lists/loop_node`, `lists/loop_fix` for the two progressions. Your goal is to make the following commands pass without any errors: `bazel test //lists/loop/...`, `bazel test --config=addrsan //lists/loop/...`, `bazel test --config=ubsan //lists/loop/...`. Adjust the directory to `loop_node` and `loop_fix` for the relevant progression. -------------------------------------------------------------------------------- /manuscript/002_lists_002_simple.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Simple operations 3 | 4 | Let's explore some basic operations frequently used as the base for a more complex solution. 5 | The three most frequent operations are: 6 | 7 | - merge two sorted lists 8 | - reverse a list 9 | - scan with two pointers 10 | 11 | Both *std::list* and *std::forward_list* come with a built-in merge operation. 12 | 13 | {caption: "Merging two sorted lists using merge()."} 14 | ```cpp 15 | #include 16 | #include 17 | 18 | { 19 | std::list left{2,4,5}; 20 | std::list right{1,3,9}; 21 | left.merge(right); 22 | // left == {1,2,3,4,5,9} 23 | // right == {} 24 | } 25 | 26 | { 27 | std::forward_list left{2,4,5}; 28 | std::forward_list right{1,3,9}; 29 | left.merge(right); 30 | // left == {1,2,3,4,5,9} 31 | // right == {} 32 | } 33 | ``` 34 | 35 | 36 | 37 | However, implementing one from scratch isn’t particularly complicated either. We consume the merged-in list, one element at a time, advancing the insertion position as needed{i: "lists!merge"}. 38 | 39 | {caption: "Custom merge operation."} 40 | ```cpp 41 | #include 42 | 43 | std::forward_list dst{1, 3, 5, 6}; 44 | std::forward_list src{2, 4, 7}; 45 | 46 | auto dst_it = dst.begin(); 47 | 48 | while (!src.empty()) { 49 | if (std::next(dst_it) == dst.end() || 50 | *std::next(dst_it) >= src.front()) { 51 | dst.splice_after(dst_it, src, src.before_begin()); 52 | } else { 53 | ++dst_it; 54 | } 55 | } 56 | // dst == {1,2,3,4,5,6,7} 57 | // src == {} 58 | ``` 59 | 60 | 61 | 62 | The same situation applies to reversing a list. Both lists provide a built-in in-place reverse operation{i: "lists|reverse"}. 63 | 64 | {caption: "Built-in in place reverse."} 65 | ```cpp 66 | #include 67 | 68 | std::forward_list src{1,2,3,4,5,6,7}; 69 | 70 | src.reverse(); 71 | // src == {7,6,5,4,3,2,1} 72 | ``` 73 | 74 | Implementing a custom reverse is straightforward if we use a second list. However, the in-place version can be tricky. 75 | 76 | {caption: "Custom implementations of linked list reverse."} 77 | ```cpp 78 | #include 79 | #include 80 | 81 | std::forward_list src{1,2,3,4,5,6,7}; 82 | 83 | // Custom reverse using a second list 84 | std::forward_list dst; 85 | while (!src.empty()) 86 | dst.splice_after(dst.before_begin(), src, src.before_begin()); 87 | // dst == {7,6,5,4,3,2,1} 88 | // src == {} 89 | 90 | // Custom in-place reverse 91 | auto tail = dst.begin(); 92 | if (tail != dst.end()) 93 | while (std::next(tail) != dst.end()) 94 | dst.splice_after(dst.before_begin(), dst, tail); 95 | // dst == {1,2,3,4,5,6,7} 96 | ``` 97 | 98 | 99 | The in-place reverse takes advantage of the fact that the first element will be the last once the list is reversed. 100 | 101 | ![Process of reversing a forward list in place.](linked_list/list_reverse_01.png) 102 | 103 | Finally, scanning with two iterators is a common search technique for finding a sequence of elements that conform to a particular property. As long as this property is calculated strictly from elements entering and leaving the sequence, we do not need to access the elements currently in the sequence{i: "two pointers|sliding window"}. 104 | 105 | {caption: "Find the longest subsequence with sum less than 4."} 106 | ```cpp 107 | #include 108 | 109 | std::forward_list data{4,2,1,1,1,3,5}; 110 | 111 | // two iterators denoting the sequence [left, right) 112 | auto left = data.begin(); 113 | auto right = data.begin(); 114 | int sum = 0; 115 | int len = 0; 116 | int max = 0; 117 | 118 | while (right != data.end()) { 119 | // extend right, until we break the property 120 | while (sum < 4 && right != data.end()) { 121 | max = std::max(max, len); 122 | ++len; 123 | sum += *right; 124 | ++right; 125 | } 126 | // shrink from left, until the property is restored 127 | while (sum >= 4 && left != right) { 128 | sum -= *left; 129 | --len; 130 | ++left; 131 | } 132 | } 133 | // max == 3, i.e. {1,1,1} 134 | ``` 135 | 136 | -------------------------------------------------------------------------------- /manuscript/003_traversal_003_problems.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Canonical problems 3 | 4 | Traversal algorithms are possibly the most frequent algorithms during technical interviews. In this section, we will limit ourselves to only five problems that exemplify the different variants of traversal algorithms we have discussed in the last two sections. 5 | 6 | ### Locked rooms 7 | 8 | Given an array of n locked rooms, each room containing *0..n* distinct keys, determine whether you can visit each room. You are given the key to room zero, and each room can only be opened with the corresponding key (however, there may be *0..n* copies of that key). 9 | 10 | Assume you can freely move between rooms; the key is the only thing you need. 11 | 12 | {class: information} 13 | B> The scaffolding for this problem is located at `traversal/locked`. Your goal is to make the following commands pass without any errors: `bazel test //traversal/locked/...`, `bazel test --config=addrsan //traversal/locked/...`, `bazel test --config=ubsan //traversal/locked/...`. 14 | 15 | ![Example of a situation where each room can be reached.](traversal/locked_rooms.png) 16 | 17 | For example, in the above situation, we can open the red lock to collect the blue and green keys, then the green lock to collect the brown key, and finally, open the remaining blue and brown locks. 18 | 19 | ### Bus routes 20 | 21 | Given a list of bus routes, where *route[i] = {b1,b2,b3}* means that bus *i* stops at stops *b1*, *b2*, and *b3*, determine the smallest number of buses you need to reach the target bus stop starting at the source. 22 | 23 | Return -1 if the target is unreachable. 24 | 25 | {class: information} 26 | B> The scaffolding for this problem is located at `traversal/buses`. Your goal is to make the following commands pass without any errors: `bazel test //traversal/buses/...`, `bazel test --config=addrsan //traversal/buses/...`, `bazel test --config=ubsan //traversal/buses/...`. 27 | 28 | ![Example of possible sequences of bus trips for different combinations of source and target stops.](traversal/bus_routes.png) 29 | 30 | In the above situation, we can reach stop six from stop one by first taking the red bus and then switching to the blue bus at stop four. 31 | 32 | ### Counting islands 33 | 34 | Given a map as a *std::vector\\>* where *'L'* represents land and *'W'* represents water, return the number of islands on the map. 35 | 36 | An island is an orthogonally (four directions) connected area of land spaces that is fully (orthogonally) surrounded by water. 37 | 38 | {class: information} 39 | B> The scaffolding for this problem is located at `traversal/islands`. Your goal is to make the following commands pass without any errors: `bazel test //traversal/islands/...`, `bazel test --config=addrsan //traversal/islands/...`, `bazel test --config=ubsan //traversal/islands/...`. 40 | 41 | ![Example of a 4x4 map with only a single island.](traversal/counting_islands.png) 42 | 43 | For example, in the above map, we only have one island since no other land masses are fully surrounded by water. 44 | 45 | ### All valid parentheses sequences 46 | 47 | Given n pairs of parentheses, generate all valid combinations of these parentheses. 48 | 49 | {class: information} 50 | B> The scaffolding for this problem is located at `traversal/parentheses`. Your goal is to make the following commands pass without any errors: `bazel test //traversal/parentheses/...`, `bazel test --config=addrsan //traversal/parentheses/...`, `bazel test --config=ubsan //traversal/parentheses/...`. 51 | 52 | ![Example of all possible valid combinations of parentheses for three pairs of parentheses.](traversal/valid_parentheses.png) 53 | 54 | For example, for *n==3* all valid combinations are: *()()()*, *(()())*, *((()))*, *(())()* and *()(())*. 55 | 56 | ### Sudoku solver 57 | 58 | Given a Sudoku puzzle as *std::vector\\>*, where unfilled spaces are represented as a space, solve the puzzle. 59 | 60 | In a solved Sudoku puzzle, each of the nine rows, columns, and *3x3* boxes must contain all digits *1..9*. 61 | 62 | {class: information} 63 | B> The scaffolding for this problem is located at `traversal/sudoku`. Your goal is to make the following commands pass without any errors: `bazel test //traversal/sudoku/...`, `bazel test --config=addrsan //traversal/sudoku/...`, `bazel test --config=ubsan //traversal/sudoku/...`. 64 | 65 | ![Example of Sudoku puzzle.](traversal/sudoku_solver.png) -------------------------------------------------------------------------------- /manuscript/004_trees_003_bst.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## BST: Binary Search Tree 3 | 4 | Binary trees are a commonly used data structure as they can efficiently encode decisions (at each node, we can progress to the left or right child), leading to *log(n)* complexity (for a balanced tree). 5 | 6 | One specific type of tree you can encounter during interviews is a binary search tree. This tree encodes a simple property. For each node, all children in the left subtree are of lower values than the value of this node, and all children in the right subtree are of higher values than the value of this node. 7 | 8 | {width: "50%"} 9 | ![Example of a balanced binary search tree.](trees/bst_example.png) 10 | 11 | A balanced binary search tree can be used as a quick lookup table, as we can lookup any value using *log(n)* operations; however, whether we will arrive at a balanced tree very much depends on the order in which elements are inserted into the tree, as the binary search tree doesn't come with any self-balancing algorithms (for that we would have to go to Red-Black trees, which is outside the scope of this book). 12 | 13 | ### Constructing a BST 14 | 15 | To construct a binary search tree, we follow the lookup logic to find a null node where the added value should be located. 16 | 17 | {caption: "Constructing a BST from a range."} 18 | ```cpp 19 | Node*& find_place_for(Node*& root, int value) { 20 | // The first empty (null) node we encounter 21 | // is the place where we want to insert. 22 | if (root == nullptr) 23 | return root; 24 | // Higher values go to the right 25 | if (root->value > value) 26 | return find_place_for(root->left, value); 27 | // Lower values go to the left 28 | if (root->value <= value) // == is for equivalent values 29 | return find_place_for(root->right, value); 30 | return root; 31 | } 32 | 33 | Tree construct_bst(const std::vector& rng) { 34 | Tree t; 35 | for (int v : rng) 36 | find_place_for(t.root, v) = t.add(v); 37 | return t; 38 | } 39 | ``` 40 | 41 | 42 | 43 | As mentioned above, the binary search tree doesn't come with any self-balancing algorithms; we can, therefore, end up in pathological situations, notably when constructing a binary search tree from a sorted input. 44 | 45 | {width: "35%"} 46 | ![Example of an unbalanced tree formed by inserting elements {1,2,3,4}.](trees/bst_unballanced.png) 47 | 48 | ### Validating a BST 49 | 50 | Binary search trees frequently appear during coding interviews as they are relatively simple, yet they encode an interesting property. 51 | 52 | The most straightforward problem (aside from constructing a BST) is validating whether a binary tree is a binary search tree. 53 | 54 | {class: information} 55 | B> Before you continue reading, I encourage you to try to solve it yourself. 56 | B> The scaffolding for this problem is located at `trees/validate_bst`. Your goal is to make the following commands pass without any errors: `bazel test //trees/validate_bst/...`, `bazel test --config=addrsan //trees/validate_bst/...`, `bazel test --config=ubsan //trees/validate_bst/...`. 57 | 58 | If we are checking a particular node in a binary search tree, going to the left subtree sets an upper bound on all the values in the left subtree and going to the right subtree sets a lower bound on all the values in the right subtree. 59 | 60 | {width: "50%"} 61 | ![Example of partitioning of values imposed by nodes in a binary search tree.](trees/bst_partitions.png) 62 | 63 | If we traverse the tree, keeping track and verifying these bounds, we will validate the BST. If we do not discover any violations, the tree is a BST; if we do, it isn't. 64 | 65 | {caption: "Validating a binary search tree."} 66 | ```cpp 67 | bool is_valid_bst(Node* root, int min, int max) { 68 | // Is this node within the bounds? 69 | if (root->value > max || root->value < min) 70 | return false; 71 | // Explore left subtree with the updated bounds 72 | if (root->left != nullptr) { 73 | if (root->value == INT_MIN) // avoid underflow 74 | return false; 75 | if (!is_valid_bst(root->left, min, root->value - 1)) 76 | return false; 77 | } 78 | // Explore right subtree with the updated bounds 79 | if (root->right != nullptr) { 80 | if (root->value == INT_MAX) // avoid overflow 81 | return false; 82 | if (!is_valid_bst(root->right, root->value + 1, max)) 83 | return false; 84 | } 85 | return true; 86 | } 87 | 88 | bool is_valid_bst(const Tree& tree) { 89 | // Root can be any value 90 | return is_valid_bst(tree.root, INT_MIN, INT_MAX); 91 | } 92 | ``` 93 | 94 | Note that this solution assumes no repeated values. To support duplicate values we would have adjust the check in the left branch, recursing with the same limit value, instead of *-1*. -------------------------------------------------------------------------------- /manuscript/004_trees_005_problems.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Canonical problems 3 | 4 | Tree problems can cover quite a range, from simple variants of the basic traversals through various variants of paths in trees to tricky problems that require non-trivial analysis for an efficient solution. 5 | 6 | This section covers three medium complexity problems: (de)serializing an n-ary tree, all nodes' k-distance, and the number of reorders of a BST. The section also covers two tricky problems: sum of distances to all nodes and well-behaved paths. 7 | 8 | ### Serialise and de-serialise n-ary tree 9 | 10 | Given an n-ary tree data structure, implement stream extraction and insertion operations that serialise and deserialise the tree. The choice of format is part of the assignment. 11 | 12 | {caption: "The tree data structure."} 13 | ```cpp 14 | struct Node { 15 | uint32_t value; 16 | std::vector children; 17 | }; 18 | 19 | struct Tree { 20 | Node* root = nullptr; 21 | // Add node to the tree, when parent == nullptr, the method sets the tree root 22 | Node* add_node(uint32_t value, Node* parent = nullptr); 23 | 24 | friend std::istream& operator>>(std::istream& s, Tree& tree); 25 | friend std::ostream& operator<<(std::ostream& s, Tree& tree); 26 | private: 27 | std::vector> storage_; 28 | }; 29 | ``` 30 | 31 | {width: "100%"} 32 | ![Serialising and deserialising an n-ary tree.](trees/serialize.png) 33 | 34 | Each node stores a *uint32_t* value and a vector of weak pointers to children. To add a node to the tree, use the *add_node* method (the method will set the tree root when the parent is *nullptr*). 35 | 36 | {class: information} 37 | B> The scaffolding for this problem is located at `trees/nary_tree`. Your goal is to make the following commands pass without any errors: `bazel test //trees/nary_tree/...`, `bazel test --config=addrsan //trees/nary_tree/...`, `bazel test --config=ubsan //trees/nary_tree/...`. 38 | 39 | ### Find all nodes of distance *k* in a binary tree 40 | 41 | Given a binary tree containing unique integer values, return all nodes that are *k* distance from the given node *n*. 42 | 43 | {width: "35%"} 44 | ![Example tree with highlighted nodes distance two from the node with value *9*.](trees/kdistance.png) 45 | 46 | {class: information} 47 | B> The scaffolding for this problem is located at `trees/kdistance`. Your goal is to make the following commands pass without any errors: `bazel test //trees/kdistance/...`, `bazel test --config=addrsan //trees/kdistance/...`, `bazel test --config=ubsan //trees/kdistance/...`. 48 | 49 | ### Sum of distances to all nodes 50 | 51 | Given a tree with n nodes, represented as a graph using a neighbourhood map, calculate the sum of distances to all other nodes for each node. 52 | 53 | {width: "50%"} 54 | ![Example of a tree with four nodes and the corresponding calculated sums of distances.](trees/sum_of_distances.png) 55 | 56 | The node ids are in the range *\[0,n\)*. 57 | 58 | {class: information} 59 | B> The scaffolding for this problem is located at `trees/sum_distances`. Your goal is to make the following commands pass without any errors: `bazel test //trees/sum_distances/...`, `bazel test --config=addrsan //trees/sum_distances/...`, `bazel test --config=ubsan //trees/sum_distances/...`. 60 | 61 | ### Well-behaved paths in a tree 62 | 63 | Given a tree, represented using two arrays of length *n*: 64 | 65 | - an array of node values, with values represented by positive integers 66 | - an array of edges represented as pairs of indexes 67 | 68 | Return the number of well-behaved paths. A well-behaved path begins and ends in a node with the same value, with all intermediate nodes being either lower or equal to the values at the ends of the path. 69 | 70 | {width: "30%"} 71 | ![Example of a tree with five single-node well-behaved paths and one four-node (dashed line) well-behaved path.](trees/well_behaved.png) 72 | 73 | {class: information} 74 | B> The scaffolding for this problem is located at `trees/well_behaved`. Your goal is to make the following commands pass without any errors: `bazel test //trees/well_behaved/...`, `bazel test --config=addrsan //trees/well_behaved/...`, `bazel test --config=ubsan //trees/well_behaved/...`. 75 | 76 | ### Number of reorders of a serialized BST 77 | 78 | Given a permutation of *1..N* as *std::vector*, return the number of other permutations that produce the same BST. The BST is produced by inserting elements in the order of their indexes (i.e. left-to-right). 79 | 80 | Because the number of permutations can be high, return the result as modulo *10^9+7*. 81 | 82 | {width: "20%"} 83 | ![Example of two reorders that lead to the same binary search tree.](trees/bst_reorder.png) 84 | 85 | {class: information} 86 | B> The scaffolding for this problem is located at `trees/bst_reorders`. Your goal is to make the following commands pass without any errors: `bazel test //trees/bst_reorders/...`, `bazel test --config=addrsan //trees/bst_reorders/...`, `bazel test --config=ubsan //trees/bst_reorders/...`. -------------------------------------------------------------------------------- /manuscript/004_trees_004_paths.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Paths in trees 3 | 4 | Another ubiquitous category of tree-oriented problems is dealing with paths in trees. Notably, paths in trees can be non-trivial to reason about, but at the same time, the tree structure still offers the possibility for very efficient solutions. 5 | 6 | A path is a sequence of nodes where every two consecutive nodes have a parent/child relationship, and each node is visited at most once. 7 | 8 | {width: "50%"} 9 | ![Example of a tree with two highlighted paths.](trees/paths_in_trees.png) 10 | 11 | Let's demonstrate this on a concrete interview problem. 12 | 13 | ### Maximum path in a tree 14 | 15 | Given a binary tree, where each node has an integer value, determine the maximum path in this tree. The value of a path is the total of all the node values visited. 16 | 17 | {class: information} 18 | B> Before you continue reading, I encourage you to try to solve it yourself. 19 | B> The scaffolding for this problem is located at `trees/maximum_path`. Your goal is to make the following commands pass without any errors: `bazel test //trees/maximum_path/...`, `bazel test --config=addrsan //trees/maximum_path/...`, `bazel test --config=ubsan //trees/maximum_path/...`. 20 | 21 | Let's consider a single node in the tree. Only four possible paths can be the maximum path that crosses this node: 22 | 23 | - a single-node path that contains this node only 24 | - a path coming from the left child, terminating in this node 25 | - a path coming from the right child, terminating in this node 26 | - a path crossing this node, i.e. going from one child, crossing this node, continuing to the other child 27 | 28 | {width: "70%"} 29 | ![The four possible paths crossing a node.](trees/paths_crossing_a_node.png) 30 | 31 | Considering the above list, we can limit the information we need to calculate the maximum path in a sub-tree whose root is the above node. 32 | 33 | If the maximum path doesn't cross this node, then the path is entirely contained in one of the child subtrees. 34 | 35 | If the path crosses this node, we can calculate the maximum path by using the information about maximum paths that terminate in the left and right child. 36 | 37 | - a single-node path is simply the value of the node 38 | - a path coming from the left child terminating in this node is the value of the maximum path terminating in the left child, plus the value of this node 39 | - a path coming from the right child terminating in this node is the value of the maximum path terminating in the right child, plus the value of this node 40 | - a path crossing this node is the value of the maximum path terminating on the right child, plus the value of the maximum path terminating in the left child, plus the value of this node 41 | 42 | The maximum path crossing this node is the maximum of the above paths. 43 | 44 | Now that we know what to calculate, we can traverse the tree in post-order (visiting the children before the parent node) while keeping track of the aforementioned values. 45 | 46 | {caption: "Solution using post-order traversal."} 47 | ```cpp 48 | // We return two values: 49 | // - the maximum path that terminates in this node 50 | // - the maximum path in this sub-tree 51 | std::pair maxPath(Tree::Node* node) { 52 | // initialize with single-node paths 53 | int max_path = node->value; 54 | int max_subtree = node->value; 55 | int full_path = node->value; 56 | 57 | if (node->left != nullptr) { 58 | // Calculate recursive values for the left path 59 | auto [path,tree] = maxPath(node->left); 60 | // Path terminating in this node: max of case 1 and 2 61 | max_path = std::max(max_path, path + node->value); 62 | // maximum path might not be crossing this node, 63 | // contained in the left subtree 64 | max_subtree = std::max(max_subtree, tree); 65 | // value of the crossing path (case 4) 66 | full_path += path; 67 | } 68 | if (node->right != nullptr) { 69 | // Calculate recursive values for the right path 70 | auto [path,tree] = maxPath(node->right); 71 | // Path terminating in this node: max of case 1 and 3 72 | // note, we already included the case 2 in the left-node if 73 | max_path = std::max(max_path, path + node->value); 74 | // maximum path might not be crossing this node, 75 | // contained in the right subtree 76 | max_subtree = std::max(max_subtree, tree); 77 | // value of the crossing path (case 4) 78 | full_path += path; 79 | } 80 | // the full path is the path starting in the left subtree, 81 | // crossing this node, continuing into the right subtree 82 | // the maximum path in this subtree is any of the paths 83 | max_subtree = std::max(max_subtree, std::max(full_path, max_path)); 84 | // max_path is the longest path terminating in this node 85 | return {max_path, max_subtree}; 86 | } 87 | 88 | // Final computation, simply return the maximum 89 | int maxPath(const Tree& t) { 90 | auto [path,tree] = maxPath(t.root); 91 | return tree; 92 | } 93 | ``` 94 | 95 | -------------------------------------------------------------------------------- /manuscript/005_binary_001_monotonic.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | # Binary search, Divide and Conquer 3 | 4 | In this chapter, we will expand on the idea of dividing a search space, which we have touched upon in the Tree chapter. 5 | 6 | Consider a binary search tree. 7 | 8 | As a reminder, a binary search tree is a tree where the values of nodes in the left child are always lower than the parent node, and the values of nodes in the right child are always higher or equal to the parent node. 9 | 10 | The structure of a binary search tree gives us a straightforward algorithm for looking up a specific value. If the value is equal to the current node, we have found our node; if the value is lower than the current node, we explore the left child, and if it is higher, we explore the right child. 11 | 12 | {width: "30%"} 13 | ![Demonstration of binary search using a binary search tree.](binary_search/tree_search.png) 14 | 15 | For a balanced tree, this will lead to *log(n)* lookup complexity as we divide our search space into half at each node. 16 | 17 | ## Binary search on monotonic sequences 18 | 19 | Instead of working with a tree, we can apply the same logic to a sorted sequence of elements. Or more formaly a non-decreasing or non-increasing sequence of elements. 20 | 21 | If we want to divide the number of elements we are searching through into half at each step, we need to compare against the middle element, which will point us to the left or right half of the sequence. 22 | 23 | {Caption: "Straightforward implementation of binary search in a contiguous range."} 24 | ```cpp 25 | bool contains(std::span data, int64_t value) { 26 | auto begin = data.begin(); 27 | auto end = data.end(); 28 | while (begin != end) { 29 | // Determine the midpoint 30 | auto mid = begin + (end-begin)/2; 31 | // We have found our value 32 | if (*mid == value) 33 | return true; 34 | // The value can only be in the right half 35 | if (*mid < value) 36 | begin = std::next(mid); 37 | // The value can only be in the left half 38 | if (*mid > value) 39 | end = mid; 40 | } 41 | return false; 42 | } 43 | ``` 44 | 45 | 46 | 47 | Fortunately, we do not have to implement binary search from scratch since the standard library contains a comprehensive set of binary search algorithms. 48 | 49 | ### *std::partition_point* 50 | 51 | It may seem strange that we are starting with the *std::partition_point* algorithm. However, *std::partition_point* is the fundamental binary search algorithm, and we can implement all other binary search algorithms in terms of *std::partition_point*. 52 | 53 | The *std::partition_point* will binary search for the first element in a partitioned range that does not satisfy the given predicate. For example, if we want to be binary search for a specific value, we can search using the predicate *e < value*. *std::partition_point* will find the first element that is not lower than the value, which means the element will be higher or equal. 54 | 55 | {Caption: "Binary search implemented in terms of std::partition_point."} 56 | ```cpp 57 | bool binary_search(std::span data, int value) { 58 | auto it = std::partition_point( 59 | data.begin(), data.end(), 60 | [value](int e) { 61 | return e < value; 62 | }); 63 | return it != data.end() && *it == value; 64 | } 65 | ``` 66 | 67 | 68 | 69 | ### *std::lower_bound* 70 | 71 | The binary search for the first element that is not lower than the given value has a named algorithm, *std::lower_bound*. 72 | 73 | {Caption: "Finding the first element not ordered before the value using std::lower_bound.} 74 | ```cpp 75 | std::vector data{1,2,3,4,5,5,5,6,6,7,8,9}; 76 | 77 | auto it = std::lower_bound(data.begin(), data.end(), 5); 78 | // *it == 5 79 | // it - data.begin() == 4 80 | ``` 81 | 82 | 83 | 84 | ### *std::upper_bound* 85 | 86 | The other important element is the first one ordered after the given value (i.e. *value < e*), which we can search for using the *std::upper_bound* algorithm. 87 | 88 | {Caption: "Finding the first element ordered after the value using std::upper_bound.} 89 | ```cpp 90 | std::vector data{1,2,3,4,5,5,5,6,6,7,8,9}; 91 | 92 | auto it = std::upper_bound(data.begin(), data.end(), 5); 93 | // *it == 6 94 | // it-data.begin() == 7 95 | ``` 96 | 97 | 98 | 99 | ### *std::equal_range* 100 | 101 | If we invoke *std::lower_bound* and *std::upper_bound* using the same value, we end up with a range of elements equal to the given value. However, we can avoid the two-step process and directly call the *std::equal_range* algorithm, which returns a pair of lower and upper bounds. 102 | 103 | {Caption: "Using std::equal_range to obtain both the lower and upper bounds."} 104 | ```cpp 105 | std::vector data{1,2,3,4,5,5,5,6,6,7,8,9}; 106 | 107 | auto [lower, upper] = std::equal_range(data.begin(), data.end(), 5); 108 | // *lower == 5, *upper = 6 109 | // lower - data.begin() == 4 110 | // upper - data.begin() == 7 111 | ``` 112 | 113 | 114 | 115 | ### *std::binary_search* 116 | 117 | The final algorithm in the family of binary searches is a simple presence check algorithm, *std::binary_search*. 118 | 119 | {Captions: "Checking for presence of values in a sorted range using std::binary_search."} 120 | ```cpp 121 | std::vector data{1,2,3,4,6,6,7,8,9}; 122 | 123 | bool t1 = std::binary_search(data.begin(), data.end(), 5); 124 | // t1 == false 125 | bool t2 = std::binary_search(data.begin(), data.end(), 6); 126 | // t2 == true 127 | ``` 128 | 129 | -------------------------------------------------------------------------------- /manuscript/004_trees_001_theory.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | # Trees 3 | 4 | Interview questions that include trees can be tricky, notably in C++. You might expect problems involving trees to be of similar complexity to linked lists. In fact, on a fundamental level, both trees and linked lists are directed graphs. However, unlike linked lists, trees do not get support from the standard C++ library. No data structure can directly represent trees, and no algorithms can directly operate on trees[^heapalgo]. 5 | 6 | [^heapalgo]: You could argue that heap algorithms fit into this category. 7 | 8 | ## Representing trees 9 | 10 | Since we cannot rely on the standard library to provide a tree data structure, we must build our own. The design options mirror our approaches when implementing a custom linked list (see. [Custom lists](#custom_lists)). 11 | 12 | The most straightforward approach for a binary tree would be to rely on *std::unique_ptr* and have each node own its children. 13 | 14 | {caption: "Flawed approach for implementing a binary tree."} 15 | ```cpp 16 | template 17 | struct TreeNode { 18 | T value = T{}; 19 | std::unique_ptr left; 20 | std::unique_ptr right; 21 | }; 22 | 23 | auto root = std::make_unique>( 24 | "root node", nullptr, nullptr); 25 | // root->value == "root node" 26 | root->left = std::make_unique>( 27 | "left node", nullptr, nullptr); 28 | // root->left->value == "left node" 29 | root->right = std::make_unique>( 30 | "right node", nullptr, nullptr); 31 | // root->right->value == "right node" 32 | ``` 33 | 34 | 35 | 36 | While this might be tempting, and notably, this approach even makes sense from an ownership perspective, this approach suffers the recursive destruction problem as the linked list. 37 | 38 | When working with well-balanced trees, the problem might not manifest; however, a forward-only linked list is still a valid binary tree. Therefore, we can easily trigger the problem. 39 | 40 | {caption: "A demonstration of a problem caused by recursive destruction."} 41 | ```cpp 42 | template 43 | struct TreeNode { 44 | T value = T{}; 45 | std::unique_ptr> left; 46 | std::unique_ptr> right; 47 | }; 48 | 49 | { 50 | auto root = std::make_unique>(0,nullptr); 51 | // Depending on the architecture/compiler, the specific number 52 | // of elements we can handle without crash will differ. 53 | TreeNode* it = root.get(); 54 | for (int i = 0; i < 100000; ++i) 55 | it = (it->left = std::make_unique>(0,nullptr)).get(); 56 | } // BOOM 57 | ``` 58 | 59 | 60 | 61 | {class: information} 62 | B> As a reminder: The recursive nature comes from the chaining of *std::unique_ptr*. As part of destroying a *std::unique_ptr\\>* we first need to destroy the child nodes, which first need to destroy their children, and so on. Each program has a limited stack space, and a sufficiently deep naive binary tree can quickly exhaust this space. 63 | 64 | While the above approach isn't quite suitable for production code, it does offer a convenient interface. For example, splicing the tree requires only calling *std::swap* on the source and destination *std::unique_ptr*, which will work even across trees. 65 | 66 | To avoid recursive destruction, we can separate the encoding of the structure of the tree from resource ownership. 67 | 68 | {caption: "A binary tree with structure and resource ownership separated."} 69 | ```cpp 70 | template 71 | struct Tree { 72 | struct Node { 73 | T value = T{}; 74 | Node* left = nullptr; 75 | Node* right = nullptr; 76 | }; 77 | Node* add(auto&& ... args) { 78 | storage_.push_back(std::make_unique( 79 | std::forward(args)...)); 80 | return storage_.back().get(); 81 | } 82 | Node* root; 83 | private: 84 | std::vector> storage_; 85 | }; 86 | 87 | Tree tree; 88 | tree.root = tree.add("root node"); 89 | // tree.root->value == "root node" 90 | tree.root->left = tree.add("left node"); 91 | // tree.root->left->value == "left node" 92 | tree.root->right = tree.add("right node"); 93 | // tree.root->right->value == "right node" 94 | ``` 95 | 96 | 97 | 98 | This approach does completely remove the recursive destruction; however, we pay for that. 99 | While we can still easily splice within a single tree, splicing between multiple trees becomes cumbersome (because it involves splicing between the resource pools). 100 | 101 | In the context of C++, neither of the above solutions is particularly performance-friendly. The biggest problem is that we are allocating each node separately, which means that they can be allocated far apart, in the worst-case situation, each node mapping to a different cache line. 102 | 103 | Conceptually, the solution is obvious: flatten the tree. However, as we are talking about performance-sensitive design, the specific details of the approach matter a lot. A serious implementation will have to take into account the specific data access pattern. 104 | 105 | The following is one possible approach for a binary tree. 106 | 107 | {caption: "One possible approach for representing a binary tree using flat data structures."} 108 | ```cpp 109 | constexpr inline size_t nillnode = 110 | std::numeric_limits::max(); 111 | 112 | template 113 | struct Tree { 114 | struct Children { 115 | size_t left = nillnode; 116 | size_t right = nillnode; 117 | }; 118 | 119 | std::vector data; 120 | std::vector children; 121 | 122 | size_t add(auto&&... args) { 123 | data.emplace_back(std::forward(args)...); 124 | children.push_back(Children{}); 125 | return data.size()-1; 126 | } 127 | size_t add_as_left_child(size_t idx, auto&&... args) { 128 | size_t cid = add(std::forward(args)...); 129 | children[idx].left = cid; 130 | return cid; 131 | } 132 | size_t add_as_right_child(size_t idx, auto&&... args) { 133 | size_t cid = add(std::forward(args)...); 134 | children[idx].right = cid; 135 | return cid; 136 | } 137 | }; 138 | 139 | Tree tree; 140 | auto root = tree.add("root node"); 141 | // tree.data[root] == "root node" 142 | auto left = tree.add_as_left_child(root, "left node"); 143 | // tree.data[left] == "left node", tree.children[root].left == left 144 | auto right = tree.add_as_right_child(root, "right node"); 145 | // tree.data[right] == "right node", tree.children[root].right == right 146 | ``` 147 | 148 | 149 | 150 | As usual, we pay for the added performance by increased complexity. We must refer to nodes through their indices since both iterators and references get invalidated during a *std::vector* reallocation. On top of that, implementing splicing for a flat tree would be non-trivial and not particularly performant as it involves re-indexing. -------------------------------------------------------------------------------- /manuscript/003_traversal_001_theory.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | # Traversal algorithms 3 | 4 | This chapter is dedicated to three algorithms we will keep revisiting in different variants throughout the book. The two search-traversal algorithms, depth-first and breadth-first search, and the constraint-traversal algorithm, backtracking. 5 | 6 | Let's start with a problem: imagine you need to find a path in a maze; how would you do it? 7 | 8 | ![Maze with one entrance and one exit.](traversal/maze_base.png) 9 | 10 | You could wander randomly, and while that might take a very long time, you will eventually reach the end. 11 | 12 | However, for a more structured approach, you might consider an approach similar to a depth-first search, exploring each branch until you reach a dead-end, then returning to the previous crossroads and taking a different path. 13 | 14 | {#dfs} 15 | ## Depth-first search 16 | 17 | The depth-first search opportunistically picks a direction at each space and explores that direction fully before returning to this space and picking a different path. 18 | 19 | A typical approach would use a consistent strategy for picking the direction order: e.g., north, south, west, east; however, as long as the algorithm explores every direction, the order doesn't matter and can be randomized. 20 | 21 | ![Depth-first search using the NSWE strategy.](traversal/maze_dfs.png) 22 | 23 | Because of the repeating nested nature, a recursive implementation is a natural fit for the depth-first search. 24 | 25 | {caption: "Recursive implementation of a depth-first search."} 26 | ```cpp 27 | bool dfs(int64_t row, int64_t col, 28 | std::vector>& map) { 29 | // Check for out-of-bounds. 30 | if (row < 0 || row == std::ssize(map) || 31 | col < 0 || col == std::ssize(map[row])) 32 | return false; 33 | 34 | // If we reached the exit, we are done. 35 | if (map[row][col] == 'E') 36 | return true; 37 | // If this is not an unvisited space, do not 38 | // terminate but, also do not continue. 39 | if (map[row][col] != ' ') 40 | return false; 41 | 42 | // Mark this space as visited. 43 | map[row][col] = '.'; 44 | 45 | return dfs(row-1,col,map) || // North 46 | dfs(row+1,col,map) || // South 47 | dfs(row,col-1,map) || // West 48 | dfs(row,col+1,map); // East 49 | } 50 | ``` 51 | 52 | 53 | 54 | We can flatten the recursive version using a stack data structure. However, we need to remember the LIFO nature of a stack. The order of exploration will be inversed from the order in which we insert the elements into the stack. 55 | 56 | {caption: "Implementation of depth-first search using a std::stack."} 57 | ```cpp 58 | bool dfs(int64_t row, int64_t col, 59 | std::vector>& map) { 60 | std::stack> next; 61 | next.push({row,col}); 62 | 63 | // As long as we have spaces to explore. 64 | while (!next.empty()) { 65 | auto [row,col] = next.top(); 66 | next.pop(); 67 | 68 | // If we reached the exit, we are done. 69 | if (map[row][col] == 'E') 70 | return true; 71 | 72 | // Mark as visited 73 | map[row][col] = '.'; 74 | 75 | // Helper to check if a space can be stepped on 76 | // i.e. not out-of-bounds and either empty or exit. 77 | auto is_path = [&map](int64_t row, int64_t col) { 78 | return row >= 0 && row < std::ssize(map) && 79 | col >= 0 && col < std::ssize(map[row]) && 80 | (map[row][col] == ' ' || map[row][col] == 'E'); 81 | }; 82 | 83 | // Due to the stack data structure we need to insert 84 | // elements in the reverse order we want to explore. 85 | if (is_path(row,col+1)) // East 86 | next.push({row,col+1}); 87 | if (is_path(row,col-1)) // West 88 | next.push({row,col-1}); 89 | if (is_path(row+1,col)) // South 90 | next.push({row+1,col}); 91 | if (is_path(row-1,col)) // North 92 | next.push({row-1,col}); 93 | } 94 | 95 | // We have explored all reachable spaces 96 | // and didn't find the exit. 97 | return false; 98 | } 99 | ``` 100 | 101 | 102 | 103 | While the depth-first search is excellent for finding a path, we don't necessarily get the shortest path. If our goal is to determine reachability, a depth-first search will be sufficient; however, if we require the path to be optimal, we must use the breadth-first search. 104 | 105 | {#bfs} 106 | ## Breadth-first search 107 | 108 | As the name suggests, the algorithm expands in breadth, visiting spaces in lock-step. The algorithm first visits all spaces next to the starting point, then all spaces next to those, i.e., two spaces away from the start, then three, four, and so on. To visualize, you can think about how water would flood the maze from the starting point. 109 | 110 | ![Breadth-first search demonstration.](traversal/maze_bfs.png) 111 | 112 | When implementing a breadth-first search, we need a data structure that will allow us to process the elements in the strict order we discover them, a queue. 113 | 114 | {caption: "Implementation of breadth-first search using a std::queue."} 115 | ```cpp 116 | int64_t bfs(int64_t row, int64_t col, std::vector>& map) { 117 | std::queue> next; 118 | next.push({row,col,0}); 119 | 120 | // As long as we have spaces to explore. 121 | while (!next.empty()) { 122 | auto [row,col,dist] = next.front(); 123 | next.pop(); 124 | 125 | // If we reached the exit, we are done. 126 | // Return the current length. 127 | if (map[row][col] == 'E') 128 | return dist; 129 | 130 | // Mark as visited. 131 | map[row][col] = '.'; 132 | 133 | // Helper to check if a space can be stepped on 134 | // i.e. not out-of-bounds and either empty or exit. 135 | auto is_path = [&map](int64_t row, int64_t col) { 136 | return row >= 0 && row < std::ssize(map) && 137 | col >= 0 && col < std::ssize(map[row]) && 138 | (map[row][col] == ' ' || map[row][col] == 'E'); 139 | }; 140 | 141 | if (is_path(row-1,col)) // North 142 | next.push({row-1,col,dist+1}); 143 | if (is_path(row+1,col)) // South 144 | next.push({row+1,col,dist+1}); 145 | if (is_path(row,col-1)) // West 146 | next.push({row,col-1,dist+1}); 147 | if (is_path(row,col+1)) // East 148 | next.push({row,col+1,dist+1}); 149 | } 150 | 151 | // We have explored all reachable spaces 152 | // and didn't find the exit. 153 | return -1; 154 | } 155 | ``` 156 | 157 | 158 | 159 | ## Backtracking 160 | 161 | Both depth-first and breadth-first searches are traversal algorithms that attempt to reach a specific goal. The difference between the two algorithms is only in the order in which they traverse the space. 162 | 163 | However, in some situations, we may not know the goal and only know the properties (constraints) the path toward the goal must fulfill. 164 | 165 | The backtracking algorithm explores the solution space in a depth-first order, discarding paths that do not fulfill the requirements. 166 | 167 | Let's take a look at a concrete example: The N-Queens problem. The goal is to place N-Queens onto an NxN chessboard without any of the queens attacking each other, i.e., no queens sharing a row, column, or diagonal. 168 | 169 | ![Demonstration of backtracking for the 4-Queens problem.](traversal/backtracking.png) 170 | 171 | The paths we explore are partial but valid solutions that build upon each other. In the above example, we traverse the solution space in row order. First, we pick a position for a queen in the first row, then second, then third, and finally fourth. The example also demonstrates two dead-ends we reach if we place the queen in the first row into the first column. 172 | 173 | A backtracking algorithm implementation will be similar to a depth-first search. However, we must keep track of the partial solution (the path), adding to the solution as we explore further and removing from the solution when we return from a dead-end. 174 | 175 | {caption: "Example implementation of backtracking."} 176 | ```cpp 177 | #include 178 | #include 179 | 180 | // Check if we can place a queen in the specified row and column 181 | bool available(std::vector& solution, 182 | int64_t row, int64_t col) { 183 | for (int64_t queen = 0; queen < std::ssize(solution); ++queen) { 184 | // Column occupied 185 | if (solution[queen] == col) 186 | return false; 187 | // NorthEast/SouthWest diagonal occupied 188 | if (row + col == queen + solution[queen]) 189 | return false; 190 | // NorthWest/SouthEast diagonal occupied 191 | if (row - col == queen - solution[queen]) 192 | return false; 193 | } 194 | return true; 195 | } 196 | 197 | bool backtrack(std::vector& solution, int64_t n) { 198 | if (std::ssize(solution) == n) 199 | return true; 200 | 201 | // We are trying to fit a queen on row std::ssize(solution) 202 | for (int64_t column = 0; column < n; ++column) { 203 | if (!available(solution, std::ssize(solution), column)) 204 | continue; 205 | 206 | // This space is not in conflict 207 | solution.push_back(column); 208 | // We found a solution, exit 209 | if (backtrack(solution, n)) 210 | return true; 211 | // This was a dead-end, remove the queen from this position 212 | solution.pop_back(); 213 | } 214 | 215 | // This is a dead-end 216 | return false; 217 | } 218 | ``` 219 | 220 | -------------------------------------------------------------------------------- /manuscript/002_lists_001_theory.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | # Linked Lists 3 | 4 | While rare in practical applications, linked lists crop up frequently in interviews. Partly this is because the node structure lends itself to formulating tricky problems, similar to trees and graphs, without the added topological complexity. 5 | 6 | ## *std::list* and *std::forward_list* 7 | 8 | The standard library offers two list types, *std::list*{i: std::list} - a doubly-linked list and *std::forward_list*{i: std::forward\_list} - a singly-linked list. 9 | The *std::forward_list* exists primarily as a space optimization, saving 8 bytes per element on 64-bit architectures. 10 | 11 | Both offer perfect iterator and reference stability, i.e., the only operation that invalidates iterators or references is the erasure of an element, and only for the removed element. The stability does extend even to moving elements between lists. 12 | 13 | {caption: "Iterator stability with std::list."} 14 | ```cpp 15 | #include 16 | 17 | std::list first{1,2,3}; 18 | std::list second{4,5,6}; 19 | 20 | // Get iterator to the element with value 2. 21 | auto it = std::next(first.begin()); 22 | 23 | // Move the element to the begining of the second list. 24 | second.splice(second.begin(), first, it); 25 | 26 | // first == {1, 3}, second == {2,4,5,6} 27 | 28 | // iterator still valid 29 | // *it == 2 30 | ``` 31 | 32 | 33 | The iterator stability is one of the use cases where we would use a *std::list* or *std::forward_list* in practical applications. The only reasonable alternative would be wrapping each element in a *std::unique_ptr*, which does offer reference stability (but not iterator stability) irrespective of the wrapping container. 34 | 35 | {caption: "Reference stability using std::unique_ptr."} 36 | ```cpp 37 | #include 38 | #include 39 | 40 | std::vector> stable; 41 | stable.push_back(std::make_unique(1)); 42 | stable.push_back(std::make_unique(2)); 43 | stable.push_back(std::make_unique(3)); 44 | 45 | // get a stable weak reference (or pointer) to an element 46 | int *it = stable[1].get(); 47 | stable.erase(stable.begin()); // invalidates all iterators 48 | // it still valid, *it == 2 49 | ``` 50 | 51 | 52 | Of course, we do pay for this stability with performance. Linked lists are node-based containers, meaning each element is allocated in a separate node, potentially very distant from each other in memory. When we combine this with the inherent overhead of the indirection, traversing a *std::list* can regularly end up 5x-10x slower than an equivalent flat *std::vector*. 53 | 54 | Aside from iterator stability, we also get access to a suite of *O(1)* operations, and these can potentially outweigh the inherent overhead of a *std::list*. 55 | 56 | {caption: "O(1) operations using a std::list and std::forward_list."} 57 | ```cpp 58 | #include 59 | 60 | std::list data{1,2,3,4,5}; 61 | 62 | // O(1) splicing between lists, or within one list 63 | 64 | // effectively rotate left by one element 65 | data.splice(data.end(), data, data.begin()); 66 | // data == {2,3,4,5,1} 67 | 68 | // O(1) erase 69 | 70 | // iterator to element with value 4 71 | auto it = std::next(data.begin(), 2); 72 | data.erase(it); 73 | // data == {2,3,5,1} 74 | 75 | // O(1) insertion 76 | 77 | // effectively push_front() 78 | data.insert(data.begin(), 42); 79 | // data = {42,2,3,5,1} 80 | ``` 81 | 82 | 83 | Because *std::list* is a bidirectional range and *std::forward_list* is a forward range, we lose access to some standard algorithms. 84 | Both lists expose custom implementations of *sort*, *unique*, *merge*, *reverse*, *remove*, and *remove_if* as member functions. 85 | 86 | {caption: "List specific algorithms."} 87 | ```cpp 88 | #include 89 | 90 | std::list data{1,2,3,4,5}; 91 | 92 | data.reverse(); 93 | // data = {5,4,3,2,1} 94 | 95 | data.sort(); 96 | // data = {1,2,3,4,5} 97 | 98 | data.remove_if([](int v) { return v % 2 == 0; }); 99 | // data == {1,3,5} 100 | ``` 101 | 102 | 103 | The *std::forward_list* has an additional quirk; since we can only erase and insert after an iterator, the *std::forward_list* offers a modified interface. 104 | 105 | {caption: "Modified interface of std::forward_list."} 106 | ```cpp 107 | #include 108 | 109 | std::forward_list data{1,2,3,4,5}; 110 | 111 | // before_begin() iterator 112 | auto it = data.before_begin(); 113 | 114 | // insert and erase only possible after the iterator 115 | data.insert_after(it, 42); 116 | // data == {42,1,2,3,4,5} 117 | data.erase_after(it); 118 | // data == {1,2,3,4,5} 119 | ``` 120 | 121 | 122 | {#custom_lists} 123 | ## Custom lists 124 | 125 | When implementing a simple custom linked list, you might be tempted to use a straightforward implementation using a *std::unique_ptr*. 126 | 127 | {caption: "A naive approach to a linked list."} 128 | ```cpp 129 | #include 130 | 131 | struct Node { 132 | int value; 133 | std::unique_ptr next; 134 | }; 135 | 136 | std::unique_ptr head = std::make_unique(20,nullptr); 137 | head->next = std::make_unique(42,nullptr); 138 | // head->value == 20 139 | // head->next->value == 42 140 | ``` 141 | 142 | 143 | 144 | 145 | Sadly, this approach isn’t usable. The fundamental problem here is the design. We are mixing ownership with structural information. In this case, this problem manifests during destruction. Because we have tied the ownership with the structure, the destruction of a list will be recursive, potentially leading to stack exhaustion and a crash. 146 | 147 | {caption: "A demonstration of a problem caused by recursive destruction."} 148 | ```cpp 149 | #include 150 | 151 | struct Node { 152 | int value; 153 | std::unique_ptr next; 154 | }; 155 | 156 | { 157 | std::unique_ptr head = std::make_unique(0,nullptr); 158 | // Depending on the architecture/compiler, the specific number 159 | // of elements we can handle without crash will differ. 160 | Node* it = head.get(); 161 | for (int i = 0; i < 100000; ++i) 162 | it = (it->next = std::make_unique(0,nullptr)).get(); 163 | } // BOOM 164 | ``` 165 | 166 | 167 | 168 | {class: information} 169 | B> The recursive nature comes from chaining *std::unique_ptr*. As part of destroying a *std::unique_ptr\* we need first to destroy the nested next pointer, which in turn needs to destroy its nested next pointer, and so on. 170 | B> A destruction of the linked list means a full expansion of destructors until we reach the end of the list. 171 | B> After reaching the end of the list, we can finally finish the destruction of the trailing node, propagating back towards the front. 172 | B> Each program has a limited stack space, and a sufficiently long naive linked list can easily exhaust this space. 173 | 174 | If we desire both the *O(1)* operations and iterator stability, the only option is to rely on manual resource management (at which point we might as well use *std::list* or *std::forward_list*). 175 | 176 | However, if we limit ourselves, there are a few alternatives to *std::list* and *std::forward_list*. 177 | 178 | If we want to capture the structure of a linked list with reference stability, we can rely on the previously mentioned combination of a *std::vector* and a *std::unique_ptr*. This approach doesn't give us any *O(1)* operations or iterator stability; however, this approach is often used during interviews{i: "lists!custom simple"}. 179 | 180 | {caption: "Representing the structure of a linked list using a std::vector and std::unique_ptr."} 181 | ```cpp 182 | #include 183 | #include 184 | 185 | struct List { 186 | struct Node { 187 | int value; 188 | Node* next; 189 | }; 190 | Node *head = nullptr; 191 | Node *new_after(Node* prev, int value) { 192 | nodes_.push_back(std::make_unique(value, nullptr)); 193 | if (prev == nullptr) 194 | return head = nodes_.back().get(); 195 | else 196 | return prev->next = nodes_.back().get(); 197 | } 198 | private: 199 | std::vector> nodes_; 200 | }; 201 | 202 | 203 | List list; 204 | auto it = list.new_after(nullptr, 1); 205 | it = list.new_after(it, 2); 206 | it = list.new_after(it, 3); 207 | 208 | // list.head->value == 1 209 | // list.head->next->value == 2 210 | // list.head->next->next->value == 3 211 | ``` 212 | 213 | The crucial difference from the naive approach is that the list data structure owns all nodes, and the structure is encoded only using weak pointers. 214 | 215 | Finally, if we do not require stable iterators or references but do require *O(1)* operations, we can use a flat list approach. 216 | We can store all elements directly in a *std::vector* and represent information about the next and previous nodes using indexes{i: "lists!custom flat"}. 217 | 218 | However, this introduces a problem. Erasing an element from the middle of a *std::vector* is *O(n)* because we need to shift successive elements to fill the gap. Since we are encoding the list structure, we can swap the to-be-erased element with the last element and only then erase it in *O(1)*. 219 | 220 | {caption: "Erase an element from the middle of a flat list in O(1)."} 221 | ```cpp 222 | #include 223 | 224 | inline constexpr ptrdiff_t nill = -1; 225 | 226 | struct List { 227 | struct Node { 228 | int value; 229 | ptrdiff_t next; 230 | ptrdiff_t prev; 231 | }; 232 | ptrdiff_t new_after(ptrdiff_t prev, int value) { 233 | storage.push_back({value, nill, prev}); 234 | if (prev != nill) 235 | storage[prev].next = std::ssize(storage)-1; 236 | else 237 | head = std::ssize(storage)-1; 238 | return std::ssize(storage)-1; 239 | } 240 | void erase(ptrdiff_t idx) { 241 | // move head 242 | if (idx == head) 243 | head = storage[idx].next; 244 | // unlink the erased element 245 | if (storage[idx].next != nill) 246 | storage[storage[idx].next].prev = storage[idx].prev; 247 | if (storage[idx].prev != nill) 248 | storage[storage[idx].prev].next = storage[idx].next; 249 | // relink the last element 250 | if (idx != std::ssize(storage)-1) { 251 | if (storage.back().next != nill) 252 | storage[storage.back().next].prev = idx; 253 | if (storage.back().prev != nill) 254 | storage[storage.back().prev].next = idx; 255 | } 256 | // swap and O(1) erase 257 | std::swap(storage[idx],storage.back()); 258 | storage.pop_back(); 259 | } 260 | ptrdiff_t get_head() { return head; } 261 | Node& at(ptrdiff_t idx) { return storage[idx]; } 262 | private: 263 | ptrdiff_t head = nill; 264 | std::vector storage; 265 | }; 266 | 267 | 268 | List list; 269 | ptrdiff_t idx = list.new_after(nill, 1); 270 | idx = list.new_after(idx, 2); 271 | idx = list.new_after(idx, 3); 272 | idx = list.new_after(idx, 4); 273 | idx = list.new_after(idx, 5); 274 | // list == {1,2,3,4,5} 275 | 276 | idx = list.get_head(); 277 | list.erase(idx); 278 | // list == {2,3,4,5} 279 | ``` 280 | 281 | -------------------------------------------------------------------------------- /manuscript/004_trees_002_traversal.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Tree traversals 3 | 4 | Before you read this section, I encourage you to familiarize yourself with [depth-first](#dfs) and [breadth-first](#bfs) search. 5 | Both searches are suitable for traversing a tree. 6 | 7 | However, for binary trees in particular, the property we care about is the specific order in which we visit the nodes of the tree. 8 | We will start with three traversals that are all based on the depth-first search. 9 | 10 | ### Pre-order traversal 11 | 12 | In pre-order traversal, we visit each node before visiting its children. 13 | 14 | {caption: "Pre-order traversal on a binary tree."} 15 | ```cpp 16 | void pre_order(Node *node, const std::function& visitor) { 17 | if (node == nullptr) return; 18 | visitor(node); 19 | pre_order(node->left, visitor); 20 | pre_order(node->right, visitor); 21 | } 22 | ``` 23 | 24 | {width: "50%"} 25 | ![Order of visiting nodes using pre-order traversal in a full binary tree.](trees/pre_order_traversal.png) 26 | 27 | A typical use case for pre-order traversal is when we need to serialise or deserialise a tree. 28 | In the following example, we serialise a binary tree as a series of space-delimited integers with missing child nodes represented by zeroes. 29 | 30 | {caption: "Serializing a binary tree of integers into a stream of space-delimited integers."} 31 | ```cpp 32 | // Serialize using pre-order traversal. 33 | void serialize(Node *node, std::ostream& s) { 34 | if (node == nullptr) { 35 | s << 0 << " "; 36 | return; 37 | } 38 | s << node->value << " "; 39 | serialize(node->left, s); 40 | serialize(node->right, s); 41 | } 42 | ``` 43 | 44 | We must have already deserialised the parent node before we can insert its children into the tree. This is why pre-order traversal is a natural fit for this use case. 45 | 46 | {caption: "Deserializing a binary tree from a stream of space-delimited integers."} 47 | ```cpp 48 | // Helper for deserializing a single node. 49 | Tree::Node *deserialize_single(Tree& tree, std::istream& s) { 50 | int value = 0; 51 | if (!(s >> value) || value <= 0) return nullptr; 52 | return tree.add(value); 53 | } 54 | 55 | // Deserialize using pre-order traversal. 56 | Tree::Node *deserialize(Tree& tree, std::istream& s) { 57 | auto node = deserialize_single(tree, s); 58 | if (node == nullptr) return node; 59 | node->left = deserialize(tree, s); 60 | node->right = deserialize(tree, s); 61 | return node; 62 | } 63 | ``` 64 | 65 | 66 | 67 | #### Non-recursive pre-order 68 | 69 | With a recursive approach, we can run into the same stack exhaustion problem we faced during tree destruction. 70 | Fortunately, similar to the baseline depth-first-search, we can switch to a non-recursive implementation by relying on a *std::stack* or *std::vector* to store the traversal state. 71 | 72 | {caption: "Non-recursive implementation of pre-order traversal."} 73 | ```cpp 74 | void pre_order_stack(Node* root, const std::function& visitor) { 75 | std::stack stack; 76 | stack.push(root); 77 | while (!stack.empty()) { 78 | Node *curr = stack.top(); 79 | stack.pop(); 80 | visitor(curr); 81 | // We visit "null" nodes with this approach, which might be helpful. 82 | if (curr == nullptr) continue; 83 | // Alternatively, we could move this condition to the push: 84 | // if (curr->right != nullptr) stack.push(curr->right); 85 | 86 | // We must insert in reverse to maintain the same 87 | // ordering as recursive pre-order. 88 | stack.push(curr->right); 89 | stack.push(curr->left); 90 | } 91 | } 92 | ``` 93 | 94 | ### Post-order traversal 95 | 96 | In post-order traversal, we visit each node after its children. 97 | 98 | {caption: "Recursive post-order traversal of a binary tree."} 99 | ```cpp 100 | void post_order(Node *node, const std::function& visitor) { 101 | if (node == nullptr) return; 102 | post_order(node->left, visitor); 103 | post_order(node->right, visitor); 104 | visitor(node); 105 | } 106 | ``` 107 | 108 | {width: "50%"} 109 | ![Order of visiting nodes using post-order traversal in a full binary tree.](trees/post_order_traversal.png) 110 | 111 | Because of this ordering, one use case for post-order is in expression trees, where we can only evaluate the parent expression if both its children were already evaluated. 112 | 113 | {caption: "Example of a simple expression tree implementation where each node contains a value or a simple operation."} 114 | ```cpp 115 | struct Eventual { 116 | std::variant> content; 119 | }; 120 | 121 | Tree tree; 122 | auto plus = [](const Eventual& l, const Eventual& r) { 123 | return get<0>(l.content) + get<0>(r.content); 124 | }; 125 | auto minus = [](const Eventual& l, const Eventual& r) { 126 | return get<0>(l.content) - get<0>(r.content); 127 | }; 128 | auto times = [](const Eventual& l, const Eventual& r) { 129 | return get<0>(l.content) * get<0>(r.content); 130 | }; 131 | // encode (4-2)*(2+1) 132 | auto root = tree.root = tree.add(Eventual{times}); 133 | auto left = root->left = tree.add(Eventual{minus}); 134 | auto right = root->right = tree.add(Eventual{plus}); 135 | left->left = tree.add(Eventual{4}); 136 | left->right = tree.add(Eventual{2}); 137 | right->left = tree.add(Eventual{2}); 138 | right->right = tree.add(Eventual{1}); 139 | 140 | post_order(tree.root, [](Node* node) { 141 | // If this node already has a result value, we don't have to do anything. 142 | if (std::holds_alternative(node->value.content)) return; 143 | // If it is an operation, then evaluate. 144 | // Post-order guarantees that node->left->value 145 | // and node->right->value are both values. 146 | node->value.content = std::get<1>(node->value.content)( 147 | node->left->value, node->right->value); 148 | }); 149 | // get<0>(root->value.content) == 6 150 | ``` 151 | 152 | 153 | 154 | #### Non-recursive post-order 155 | 156 | For a non-recursive approach, we could visit all nodes in pre-order, remembering each, and then iterate over the nodes in reverse order. 157 | However, we can do better. 158 | 159 | The main problem we must solve is remembering enough information to correctly decide whether it is time to visit the parent node. The following approach eagerly explores the left sub-tree, remembering both the right sibling and the parent node. When we revisit the parent node, we can decide whether it is time to visit it based on the presence of the right sibling. 160 | 161 | {caption: "Non-recursive post-order traversal with only partial memoization."} 162 | ```cpp 163 | void post_order_nonrecursive(Node *root, const std::function& visitor) { 164 | std::stack s; 165 | Node *current = root; 166 | while (true) { 167 | // Explore left, but remember node & right child. 168 | if (current != nullptr) { 169 | if (current->right != nullptr) 170 | s.push(current->right); 171 | s.push(current); 172 | current = current->left; 173 | continue; 174 | } 175 | // current == nullptr 176 | if (s.empty()) return; 177 | current = s.top(); 178 | s.pop(); 179 | // If we have the right child remembered, 180 | // it would be on the top of the stack. 181 | if (current->right && !s.empty() && current->right == s.top()) { 182 | // If it is, we must visit it (and it's children) first. 183 | s.pop(); 184 | s.push(current); 185 | current = current->right; 186 | } else { 187 | // If the top of the stack is not the right child, 188 | // we have already visited it. 189 | visitor(current); 190 | current = nullptr; 191 | } 192 | } 193 | } 194 | ``` 195 | 196 | ### In-order traversal 197 | 198 | In in-order traversal, we visit each node in between visiting its left and right children. 199 | 200 | Unlike pre- and post-order traversals that are relatively general, and we can easily apply them to n-ary trees, in-order traversal only makes sense in the context of binary trees. 201 | 202 | {caption: "Recursive in-order traversal of a binary tree."} 203 | ```cpp 204 | // in-order traversal 205 | void in_order(Node* node, const std::function& visitor) { 206 | if (node == nullptr) return; 207 | in_order(node->left, visitor); 208 | visitor(node); 209 | in_order(node->right, visitor); 210 | } 211 | ``` 212 | 213 | {width: "50%"} 214 | ![Order of visiting nodes using in-order traversal in a full binary tree.](trees/in_order_traversal.png) 215 | 216 | The typical use case for in-order traversal is for traversing binary trees that encode an ordering of elements. The in-order traversal naturally maintains this order during the traversal. 217 | 218 | {caption: "Traversing a BST to produce a sorted output."} 219 | ```cpp 220 | // Insert an element into the tree in sorted order 221 | void add_sorted(Tree& tree, Node* node, int64_t value) { 222 | if (value <= node->value) { 223 | if (node->left == nullptr) 224 | node->left = tree.add(value); 225 | else 226 | add_sorted(tree, node->left, value); 227 | } else { 228 | if (node->right == nullptr) 229 | node->right = tree.add(value); 230 | else 231 | add_sorted(tree, node->right, value); 232 | } 233 | } 234 | 235 | Tree tree; 236 | // Generate a sorted binary tree with 10 nodes 237 | std::mt19937 gen(0); // change the seed for a different output 238 | std::uniform_int_distribution<> dist(0,1000); 239 | tree.root = tree.add(dist(gen)); 240 | for (int i = 0; i < 9; i++) { 241 | add_sorted(tree, tree.root, dist(gen)); 242 | } 243 | 244 | // in-order traversal will print the values in sorted order 245 | in_order(tree.root, [](Node* node) { 246 | std::cout << node->value << " "; 247 | }); 248 | std::cout << "\n"; 249 | // stdlibc++: 424 545 549 593 603 624 715 845 848 858 250 | // libc++: 9 192 359 559 629 684 707 723 763 835 251 | ``` 252 | 253 | #### Non-recursive in-order 254 | 255 | The non-recursive approach is similar to post-order, but we avoid the complexity of remembering the right child. 256 | 257 | {caption: "Non-recursive in-order traversal implementation."} 258 | ```cpp 259 | void in_order_nonrecursive(Node *root, const std::function& visitor) { 260 | std::stack s; 261 | Node *current = root; 262 | while (current != nullptr || !s.empty()) { 263 | // Explore left 264 | while (current != nullptr) { 265 | s.push(current); 266 | current = current->left; 267 | } 268 | // Now going back up the left path visit each node, 269 | // then explore the right child. 270 | // This works, because the left child was already 271 | // visited as we go up the path. 272 | current = s.top(); 273 | s.pop(); 274 | visitor(current); 275 | current = current->right; 276 | } 277 | } 278 | ``` 279 | 280 | 281 | 282 | ### Rank-order traversal 283 | 284 | The rank-order or level-order traversal traverses nodes in the order of their distance from the root node. 285 | 286 | All the previous traversals: pre-order, post-order and in-order are based on depth-first search, rank-order traversal is based on breadth-first search, naturally avoiding the recursion problem. 287 | 288 | {caption: "Rank-order traversal implementation."} 289 | ```cpp 290 | void rank_order(Node* root, const std::function& visitor) { 291 | std::queue q; 292 | if (root != nullptr) 293 | q.push(root); 294 | while (!q.empty()) { 295 | Node* current = q.front(); 296 | q.pop(); 297 | if (current == nullptr) continue; 298 | visitor(current); 299 | q.push(current->left); 300 | q.push(current->right); 301 | } 302 | } 303 | ``` 304 | 305 | {width: "50%"} 306 | ![Order of visiting nodes using rank-order traversal in a full binary tree.](trees/rank_order_traversal.png) 307 | 308 | Rank-order traversal typically comes up as part of more complex problems. 309 | By default, it can be used to find the closest node to the root that satisfies particular criteria or calculate the nodes' distance from the root. 310 | 311 | {caption: "Calculating the maximum node value at each level of the tree."} 312 | ```cpp 313 | std::vector max_at_level(Node* root) { 314 | std::vector result; 315 | std::queue> q; 316 | if (root != nullptr) 317 | q.push({root,0}); 318 | while (!q.empty()) { 319 | auto [node,rank] = q.front(); 320 | q.pop(); 321 | if (result.size() <= rank) 322 | result.push_back(node->value); 323 | else 324 | result[rank] = std::max(result[rank], node->value); 325 | if (node->left != nullptr) 326 | q.push({node->left,rank+1}); 327 | if (node->right != nullptr) 328 | q.push({node->right, rank+1}); 329 | } 330 | return result; 331 | } 332 | ``` 333 | 334 | -------------------------------------------------------------------------------- /manuscript/002_lists_005_solutions.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Solutions 3 | 4 | ### Reverse k-groups in a list 5 | 6 | There isn’t anything algorithmically complex in this problem. However, there are many opportunities to trip up and make a mistake. 7 | 8 | We talked about reversing a singly-linked list in the simple operations section. This operation will be our base for reversing the group of k elements. 9 | 10 | {caption: "Reversing a group of k elements."} 11 | ```cpp 12 | List::Node* reverse(List::Node *head, int64_t k) { 13 | List::Node *result = nullptr; 14 | List::Node *iter = head; 15 | 16 | for (int64_t i = 0; i < k; ++i) 17 | iter = std::exchange(iter->next, std::exchange(result, iter)); 18 | /* we want to put iter to the front of the list 19 | but we want the original value of result to be the new iter->next 20 | but we want the original value of iter->next to be the new iter 21 | */ 22 | 23 | return result; 24 | } 25 | ``` 26 | 27 | The complexity lies in applying this operation multiple times in sequence. For that, we need to keep track of terminating nodes: 28 | 29 | - the head of the already processed part; this will be our final result 30 | - the tail of the already processed part; this is where we will attach each reversed section as we iterate 31 | - the head of the unprocessed part; for iteration and we also need it to relink the reversed group of k elements 32 | 33 | ![Keeping track of nodes](linked_list/list_reverse_kgroup_solution_01.png) 34 | 35 | The algorithm repeats these steps: 36 | 37 | ![Calculate the kth element by advancing k steps from the unprocessed head.](linked_list/list_reverse_kgroup_solution_02.png) 38 | ![Reverse k elements starting from the unprocessed head.](linked_list/list_reverse_kgroup_solution_03.png) 39 | ![Connect the reversed group to the tail of the processed part. Connect the unprocessed head to the kth element.](linked_list/list_reverse_kgroup_solution_04.png) 40 | ![The new tail is the unprocessed head. The new unprocessed head is the kth element.](linked_list/list_reverse_kgroup_solution_05.png) 41 | 42 | {caption: "Solution"} 43 | ```cpp 44 | void reverse_groups(List &list, int64_t k) { 45 | List::Node *unprocessed_head = list.head; 46 | List::Node *processed_tail = nullptr; 47 | List::Node *result = nullptr; 48 | List::Node *iter = list.head; 49 | 50 | while (iter != nullptr) { 51 | // advance by k elements 52 | int cnt = 0; 53 | iter = unprocessed_head; 54 | while (cnt < k && iter != nullptr) { 55 | iter = iter->next; 56 | ++cnt; 57 | } 58 | 59 | // if we have a full set of k elements 60 | if (cnt == k) { 61 | List::Node *processed_head = reverse(unprocessed_head, k); 62 | // initialize the result if this is the first group 63 | if (result == nullptr) 64 | result = processed_head; 65 | // if this isn't the first group, link the existing tail 66 | if (processed_tail != nullptr) 67 | processed_tail->next = processed_head; 68 | 69 | // what was head is now the tail of the reversed section 70 | processed_tail = unprocessed_head; 71 | // and iter is the new head 72 | unprocessed_head = iter; 73 | } 74 | } 75 | if (processed_tail != nullptr) 76 | processed_tail->next = unprocessed_head; 77 | list.head = result == nullptr ? unprocessed_head : result; 78 | } 79 | ``` 80 | 81 | 82 | We access each element at most twice. Once when advancing by k elements and the second when we are reversing a group of k elements. This means that our time complexity is *$O(n)*, and since we only store the terminal nodes, our space complexity is *O(1)*. 83 | 84 | ### Merge a list of sorted lists 85 | 86 | We already discussed merging two lists in the simple operations section. However, we need to be careful here. If we would merge-in each list in a loop, we would end up with *$O(k\*n)* time complexity, where *k* is the number of lists and *n* is the number of nodes. 87 | 88 | The desired *O(n\*log(k))* time complexity should point you towards some form of a sorted structure(*std::priority_queue*, heap algorithms, *std::set*). A sorted structure would give us *log(k)* lookup, which we can then repeat *n* times to traverse all the elements in order. 89 | 90 | The second way to reach *log* scaling is with an exponential factor. If we merge lists in pairs, we will half the number of lists in each step, with total *log(k)* steps, leaving us again with *O(n\*log(k))* complexity. 91 | 92 | In either case, we must be careful not to introduce accidental copies. 93 | 94 | {caption: "Solution using an always-sorted structure."} 95 | ```cpp 96 | std::forward_list merge(std::forward_list> in) { 97 | using fwlst = std::forward_list; 98 | 99 | // Custom comparator, compare based on the first element 100 | auto cmp = [](const fwlst& l, const fwlst& r) { 101 | return l.front() < r.front(); 102 | }; 103 | // View of non-empty lists, if we filter here, 104 | // we don't have to check later. 105 | auto non_empty = in | 106 | std::views::filter([](auto& l) { return !l.empty(); }) | 107 | std::views::common; 108 | // Consume the input using std::move_iterator, 109 | // avoids making copies of the lists. 110 | std::multiset q( 111 | std::move_iterator(non_empty.begin()), 112 | std::move_iterator(non_empty.end()), 113 | cmp); 114 | 115 | fwlst result; 116 | fwlst::iterator tail = result.before_begin(); 117 | while (!q.empty()) { 118 | // Extract the node that holds the element, 119 | // without making a copy 120 | auto top = q.extract(q.begin()); 121 | 122 | // Splice the first element of the top list to the result 123 | result.splice_after(tail, 124 | top.value(), top.value().before_begin()); 125 | ++tail; 126 | 127 | if (!top.value().empty()) 128 | q.insert(std::move(top)); // put back 129 | } 130 | return result; 131 | } 132 | ``` 133 | 134 | Because we extract each element once and each extract operation involves *O(log(k))* insertion operation, we end up with *O(n\*log(k))* time complexity. Our *std::multiset* will use *O(k)* memory. 135 | 136 | {caption: "Solution using pairwise merging."} 137 | ```cpp 138 | std::forward_list merge(std::forward_list> in) { 139 | std::forward_list> merged; 140 | // While we have more than one list 141 | while (std::next(in.begin()) != in.end()) { 142 | auto it = in.begin(); 143 | // Take a pair of lists 144 | while (it != in.end() && std::next(it) != in.end()) { 145 | // Merge 146 | it->merge(*std::next(it)); 147 | merged.emplace_front(std::move(*it)); 148 | std::advance(it, 2); 149 | } 150 | // If we have odd number of lists 151 | if (it != in.end()) 152 | merged.emplace_front(std::move(*it)); 153 | // Switch the lists for the next iteration 154 | in = std::move(merged); 155 | } 156 | return std::move(in.front()); 157 | } 158 | ``` 159 | 160 | We merge *n* elements in every iteration, repeating this for *log(k)* iterations, leading to *O(n\*log(k))* time complexity. The only additional memory we use is to store the partially merged lists; therefore, we end up with *O(k)* space complexity. 161 | 162 | 163 | 164 | ### Remove the kth element from the end of a list 165 | 166 | The trivial approach would be to check whether we are at the kth element from the back and, if not, advance to the next element. However, this approach would have *O(n\*k)* time complexity. 167 | 168 | Once we check whether an element is the kth element from the back, we have two iterators that are *k-1* elements apart. 169 | To check the next element, we do not have to repeat the entire process; instead, we can advance the iterator pointing to the previous *k-1* apart element, again ending with a pair of elements that are *k-1* apart. 170 | 171 | Extending this idea allows us to implement an *O(n)* solution. We calculate the first pair of elements that k apart, and from there, we advance both iterators in step until we reach the end of the list. 172 | 173 | {caption: "Solution"} 174 | ```cpp 175 | void remove_kth_from_end(std::forward_list& head, int64_t k) { 176 | auto node = head.before_begin(); 177 | auto tail = head.begin(); 178 | // advance the tail by k-1 steps 179 | for (int64_t i = 1; i < k && tail != head.end(); ++i) 180 | ++tail; 181 | 182 | // there is no k-the element from the back 183 | if (tail == head.end()) return; 184 | 185 | // advance both node and tail, until we reach the end with tail 186 | // this means that node is poiting to the k-th element from the back 187 | while (std::next(tail) != head.end()) { 188 | ++node; 189 | ++tail; 190 | } 191 | 192 | // remove the node 193 | head.erase_after(node); 194 | } 195 | ``` 196 | 197 | 198 | 199 | ### Find a loop in a linked list 200 | 201 | This problem is the prototypical problem for the fast and slow pointer technique (a.k.a. Floyd’s tortoise and hare). 202 | 203 | We will eventually reach the end if we iterate over the list without a loop. However, if there is a loop, we will end up stuck in the loop. 204 | 205 | The tricky part is detecting that we are stuck in the loop. If we use two pointers to iterate, one slow one, iterating normally and one fast one, iterating over two nodes in each step, we have a guarantee that if they get stuck in a loop, they will eventually meet. 206 | 207 | ![Initial configuration: slow and fast pointers are pointing to the head of the list.](linked_list/list_loop_detect_01.png) 208 | 209 | ![Configuration after two steps.](linked_list/list_loop_detect_02.png) 210 | 211 | ![Configuration after four steps.](linked_list/list_loop_detect_03.png) 212 | 213 | ![Configuration after six steps. Loop detected.](linked_list/list_loop_detect_04.png) 214 | 215 | {captions: "Detecting a loop"} 216 | ```cpp 217 | bool loop_detection(const List &list) { 218 | List::Node *slow = list.head; 219 | List::Node *fast = list.head; 220 | do { 221 | // nullptr == no loop 222 | if (slow == nullptr) 223 | return false; 224 | if (fast == nullptr || fast->next == nullptr) 225 | return false; 226 | slow = slow->next; 227 | fast = fast->next->next; 228 | } while (slow != fast); 229 | 230 | return true; 231 | } 232 | ``` 233 | 234 | #### Identifying the start of the loop 235 | 236 | To detect the start of the loop, we must look at how many steps both pointers made before they met up. 237 | 238 | Consider that the slow pointer moved *x* steps before entering the loop and then *y* steps after entering the loop for a *slow = x + y* total. 239 | 240 | The fast pointer moved similarly. It also moved *x* steps before entering the loop and then *y* steps after entering the loop when it met up with the slow pointer; however, before that, it did an unknown number of loops: *fast = x + n\*loop + y*. Importantly, we also know that the fast pointer also did *2\*slow* steps. 241 | 242 | If we put this together, we end up with the following: 243 | 244 | - *2\*(x + y) = x + n\*loop + y* 245 | - *x = n\*loop - y* 246 | 247 | This means that the number of steps to reach the loop is the same as the number of steps remaining from where the pointers met up to the start of the loop. 248 | 249 | So to find the start of the loop, we can iterate from the start and the meeting point. Once these two new pointers meet, we have our loop start. 250 | 251 | ![One pointer at the meeting point, one at the list head.](linked_list/list_loop_start_01.png) 252 | 253 | ![The loop start is identified after two steps.](linked_list/list_loop_start_02.png) 254 | 255 | {caption: "Solution with start detection."} 256 | ```cpp 257 | List::Node *loop_start(const List &list) { 258 | // Phase 1, detect the loop. 259 | List::Node *slow = list.head; 260 | List::Node *fast = list.head; 261 | do { 262 | // nullptr == no loop 263 | if (slow == nullptr) 264 | return nullptr; 265 | if (fast == nullptr || fast->next == nullptr) 266 | return nullptr; 267 | slow = slow->next; 268 | fast = fast->next->next; 269 | } while (slow != fast); 270 | 271 | // Phase 2, iterate from head and from meeting point. 272 | List::Node *onloop = slow; 273 | List::Node *offloop = list.head; 274 | while (onloop != offloop) { 275 | onloop = onloop->next; 276 | offloop = offloop->next; 277 | } 278 | return onloop; 279 | } 280 | ``` 281 | 282 | #### Fixing the list 283 | 284 | The main difficulty in fixing the list is that we are working with a singly-linked list. Fixing the list means that we must unlink node one before the start of the loop. 285 | 286 | {caption: "Solution for fixing the list."} 287 | ```cpp 288 | void loop_fix(List &list) { 289 | // Phase 1, detect the loop. 290 | List::Node *slow = list.head; 291 | List::Node *fast = list.head; 292 | List::Node *before = nullptr; 293 | do { 294 | // nullptr == no loop 295 | if (slow == nullptr) 296 | return; 297 | if (fast == nullptr || fast->next == nullptr) 298 | return; 299 | slow = slow->next; 300 | // Keep track of the node one before the fast pointer 301 | before = fast->next; 302 | fast = fast->next->next; 303 | } while (slow != fast); 304 | 305 | // Phase 2, iterate from head and from meeting point. 306 | List::Node *onloop = slow; 307 | List::Node *offloop = list.head; 308 | while (onloop != offloop) { 309 | // Keep track of the node one before the onloop pointer 310 | before = onloop; 311 | onloop = onloop->next; 312 | offloop = offloop->next; 313 | } 314 | 315 | // Phase 3, fix the list, before != nullptr 316 | before->next = nullptr; 317 | } 318 | ``` -------------------------------------------------------------------------------- /manuscript/003_traversal_002_variants.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Notable variants 3 | 4 | The traversal algorithms mentioned earlier are largely standalone, ready to be deployed to solve diverse problems with minimal tweaks. 5 | 6 | Yet, we frequently encounter a few additional versions. In this section, we'll tackle three such variants: traversing multiple dimensions, adjusting for non-unit costs, and managing the propagation of constraints. 7 | 8 | We'll also illustrate each variant using a concrete problem, all of which you can find in the companion repository. Try solving each of them before you read the corresponding solution. 9 | 10 | ### Multi-dimensional traversal 11 | 12 | Applying a depth-first or breadth-first search to a problem with additional spatial dimensions is straightforward. From the algorithm's perspective, additional dimensions only introduce a broader neighborhood for each space. However, in some problems, the additional dimensions will not be that obvious. 13 | 14 | Consider the following problem: Given a 2D grid of size *m\*n*, containing *0s* (spaces) and *1s* (obstacles), determine the length of the shortest path from the coordinate *{0,0}* to *{m-1,n-1}*, given that you can remove up to *k* obstacles. 15 | 16 | {class: tip} 17 | B> Before you continue reading, try solving this problem yourself. The scaffolding for this problem is located at `traversal/obstacles`. Your goal is to make the following commands pass without any errors: `bazel test //traversal/obstacles/...`, `bazel test --config=addrsan //traversal/obstacles/...`, `bazel test --config=ubsan //traversal/obstacles/...`. 18 | 19 | Because we are looking for the shortest path, we don't have a choice of the traversal algorithm. We must use a breadth-first search. But how do we deal with the obstacles? 20 | 21 | Let's consider adding a 3rd dimension to the problem. Instead of removing an obstacle, we can virtually move to a new maze floor, where this obstacle never existed. However, this introduces a problem. We can't apply this logic mindlessly since there are potentially *m\*n* obstacles. 22 | 23 | Fortunately, we can lean on the behaviour of breadth-first search. When we enter a new floor of the maze, we have a guarantee that we will never revisit the space we entered through. This means we do not have to track which specific obstacles we removed, only how many we can still remove, shrinking the number of floors to *k+1*. Applying a breadth-first search then leaves us with a total time complexity of *O(m\*n\*(k+1))*. 24 | 25 | {caption: "Breadth-first search in a maze with obstacle removal."} 26 | ```cpp 27 | #include 28 | #include 29 | #include 30 | 31 | struct Dir { 32 | int64_t row; 33 | int64_t col; 34 | }; 35 | 36 | struct Pos { 37 | int64_t row; 38 | int64_t col; 39 | int64_t k; 40 | int64_t distance; 41 | }; 42 | 43 | int shortest_path(const std::vector>& grid, int64_t k) { 44 | // Keep track of visited spaces, initialize all spaces as unvisited. 45 | std::vector>> visited( 46 | grid.size(), std::vector> ( 47 | grid[0].size(), std::vector(k+1, false) 48 | ) 49 | ); 50 | 51 | // BFS 52 | std::queue q; 53 | // start in {0,0} with zero removed obstacles 54 | q.push(Pos{0,0,0,0}); 55 | visited[0][0][0] = true; 56 | 57 | while (!q.empty()) { 58 | auto current = q.front(); 59 | q.pop(); 60 | // The first time we visit the end coordinate is the shortest path 61 | if (current.row == std::ssize(grid)-1 && 62 | current.col == std::ssize(grid[current.row])-1) { 63 | return current.distance; 64 | } 65 | 66 | // For every direction, try to move there 67 | for (auto dir : {Dir{-1,0}, Dir{1,0}, Dir{0,-1}, Dir{0,1}}) { 68 | // This space is out of bounds, ignore. 69 | if ((current.row + dir.row < 0) || 70 | (current.col + dir.col < 0) || 71 | (current.row + dir.row >= std::ssize(grid)) || 72 | (current.col + dir.col >= std::ssize(grid[0]))) 73 | continue; 74 | 75 | // If the space in the current direction is an empty space: 76 | Pos empty = {current.row + dir.row, current.col + dir.col, 77 | current.k, current.distance + 1}; 78 | if (grid[empty.row][empty.col] == 0 && 79 | !visited[empty.row][empty.col][empty.k]) { 80 | // add it to the queue 81 | q.push(empty); 82 | // and mark as visited 83 | visited[empty.row][empty.col][empty.k] = true; 84 | } 85 | 86 | // If we have already removed k obstacles, 87 | // we don't consider removing more. 88 | if (current.k == k) 89 | continue; 90 | 91 | // If the space in the current direction is an obstacle: 92 | Pos wall = {current.row + dir.row, current.col + dir.col, 93 | current.k + 1, current.distance + 1}; 94 | if (grid[wall.row][wall.col] == 1 && 95 | !visited[wall.row][wall.col][wall.k]) { 96 | // add it to the queue 97 | q.push(wall); 98 | // and mark as visited 99 | visited[wall.row][wall.col][wall.k] = true; 100 | } 101 | } 102 | } 103 | 104 | // If we are here, we did not reach the end coordinate. 105 | return -1; 106 | } 107 | ``` 108 | 109 | 110 | 111 | ### Shortest path with non-unit costs 112 | 113 | In all the problems we discussed, the cost of moving from one space to another was always one unit. The breadth-first search algorithm explicitly relies on this property to process spaces strictly by distance. 114 | 115 | Therefore if we are working with a problem that doesn't have unit cost, we must adjust our approach. 116 | 117 | Consider the following problem: Given a 2D heightmap of size *m\*n*, where negative integers represent impassable terrain and positive integers represent the terrain height, determine the shortest path between the two given coordinates under the following constraints: 118 | 119 | - the path cannot cross impassable terrain 120 | - moving on a level terrain costs two time-units 121 | - moving downhill costs one time-unit 122 | - moving uphill costs four time-units 123 | 124 | {class: tip} 125 | B> Before you continue reading, try solving this problem yourself. The scaffolding for this problem is located at `traversal/heightmap`. Your goal is to make the following commands pass without any errors: `bazel test //traversal/heightmap/...`, `bazel test --config=addrsan //traversal/heightmap/...`, `bazel test --config=ubsan //traversal/heightmap/...`. 126 | 127 | The primary requirement of BFS is that we process elements in the order of their distance from the start of the path. When all transitions have a unit cost, we can achieve this by relying on a queue. However, with non-unit costs, we must use an ordered structure such as *std::priority_queue*. Note that switching to a priority queue will affect the time complexity as we are moving from *O(1)* *push* and *pop* operations to *O(log(n))* *push* and *pop* operations. 128 | 129 | The second guarantee we lose concerns the shortest path when we first push a space into the queue. If we discovered a space with a path length *X* we had a guarantee that all later paths that also lead to this space would, at best equal *X*. Because of this constraint, we could limit ourselves to adding each space into the queue only once. With non-unit costs, this property no longer holds. 130 | 131 | It is possible that a later (and longer) path can enter the same space with an overall shorter path length. Consequently, we might need to insert a space multiple times into our queue (bounded by the number of neighbours). However, we still have a slightly weaker but still significant guarantee. The ordered nature of the priority queue guarantees that the first time we pop a space from the queue, it is part of the shortest path that enters this space. 132 | 133 | Due to the queue's logarithmic complexity, we end up with *O(m\*n\*log(m\*n))* overall time complexity for the breadth-first search. 134 | 135 | {caption: "Breadth-first search using a priority queue to handle non-unit costs."} 136 | ```cpp 137 | #include 138 | #include 139 | #include 140 | 141 | struct Coord { 142 | int64_t row; 143 | int64_t col; 144 | }; 145 | 146 | int64_t shortest_path(const std::vector>& map, 147 | Coord start, Coord end) { 148 | struct Pos { 149 | int64_t length; 150 | Coord coord; 151 | }; 152 | 153 | // For tracking visited spaces 154 | std::vector> visited(map.size(), 155 | std::vector(map[0].size(), false)); 156 | 157 | // Helper to check whether a space can be stepped on 158 | // not out of bounds, not impassable and not visited 159 | auto can_step = [&map, &visited](Coord coord) { 160 | auto [row, col] = coord; 161 | return row >= 0 && col >= 0 && 162 | row < std::ssize(map) && col < std::ssize(map[row]) && 163 | map[row][col] >= 0 && 164 | !visited[row][col]; 165 | }; 166 | 167 | // Priority queue instead of a simple queue 168 | std::priority_queue, 169 | decltype([](const Pos& l, const Pos& r) { 170 | return l.length > r.length; 171 | })> q; 172 | // Start with path length zero at start 173 | q.push({0,start}); 174 | 175 | // Helper to determine the cost of moving between two spaces 176 | auto step_cost = [&map](Coord from, Coord to) { 177 | if (map[from.row][from.col] < map[to.row][to.col]) return 4; 178 | if (map[from.row][from.col] > map[to.row][to.col]) return 1; 179 | return 2; 180 | }; 181 | 182 | while (!q.empty()) { 183 | // Grab the position closest to the start 184 | auto [length, pos] = q.top(); 185 | q.pop(); 186 | 187 | if (visited[pos.row][pos.col]) continue; 188 | // The first time we grab a position from the queue is guaranteed 189 | // to be the shortest path, so now we need to mark it as visited. 190 | // If we later visit the same position (already in queue at this point) 191 | // with a longer path, we skip it based on the above check. 192 | visited[pos.row][pos.col] = true; 193 | 194 | // First time we would try to exit the end space is the shortest path. 195 | if (pos.row == end.row && pos.col == end.col) 196 | return length; 197 | 198 | // Expand to all four directions 199 | for (auto next : {Coord{pos.row-1,pos.col}, 200 | Coord{pos.row+1, pos.col}, 201 | Coord{pos.row, pos.col-1}, 202 | Coord{pos.row, pos.col+1}}) { 203 | if (!can_step(next)) continue; 204 | q.push({length + step_cost(pos, next), next}); 205 | } 206 | } 207 | 208 | return -1; 209 | } 210 | ``` 211 | 212 | 213 | 214 | ### Constraint propagation 215 | 216 | In the previous section, we used backtracking to solve the N-Queens problem. However, if you look at the implementation, we repeatedly check each new queen against all previously placed queens. We can do better. 217 | 218 | When working with backtracking, we cannot escape the inherent exponential complexity of the worst case. However, we can often significantly reduce the exponents by propagating the problem's constraints forward. The main objective is to remove as many options from the consideration altogether by ensuring that the constraints are maintained 219 | 220 | {class: tip} 221 | B> Before you continue reading, try modifying the previous version yourself. The scaffolding for this problem is located at `traversal/queens`. Your goal is to make the following commands pass without any errors: `bazel test //traversal/queens/...`, `bazel test --config=addrsan //traversal/queens/...`, `bazel test --config=ubsan //traversal/queens/...`. 222 | 223 | Specifically for the N-Queens problem, we have *N* rows, *N* columns, *2\*N-1* NorthWest, and *2\*N-1* NorthEast diagonals. Placing a queen translates to claiming one row, column, and the corresponding diagonals. Instead of checking each queen against all previous queens, we can limit ourselves to checking whether the corresponding row, column, or one of the two diagonals was already claimed. 224 | 225 | {caption: "Solving the N-Queens problem with backtracking and constraint propagation."} 226 | ```cpp 227 | // Helper to store the current state: 228 | struct State { 229 | State(int64_t n) : n(n), solution{}, cols(n), nw_dia(2*n-1), ne_dia(2*n-1) {} 230 | // Size of the problem. 231 | int64_t n; 232 | // Partial solution 233 | std::vector solution; 234 | // Occupied columns 235 | std::vector cols; 236 | // Occupied NorthWest diagonals 237 | std::vector nw_dia; 238 | // Occupied NorthEast diagonals 239 | std::vector ne_dia; 240 | // Check column, and both diagonals 241 | bool available(int64_t row, int64_t col) const { 242 | return !cols[col] && !nw_dia[row-col+n-1] && !ne_dia[row+col]; 243 | } 244 | // Mark this position as occupied and add it to the partial solution 245 | void mark(int64_t row, int64_t col) { 246 | solution.push_back(col); 247 | cols[col] = true; 248 | nw_dia[row-col+n-1] = true; 249 | ne_dia[row+col] = true; 250 | } 251 | // Unmark this position as occupied and remove it from the partial solution 252 | void erase(int64_t row, int64_t col) { 253 | solution.pop_back(); 254 | cols[col] = false; 255 | nw_dia[row-col+n-1] = false; 256 | ne_dia[row+col] = false; 257 | } 258 | }; 259 | 260 | bool backtrack(auto& state, int64_t row, int64_t n) { 261 | // All Queens have their positions, we have solution 262 | if (row == n) return true; 263 | 264 | // Try to find a feasible column on this row 265 | for (int c = 0; c < n; ++c) { 266 | if (!state.available(row,c)) 267 | continue; 268 | // Mark this position 269 | state.mark(row,c); 270 | // Recurse to the next row 271 | if (backtrack(state, row+1, n)) 272 | return true; // We found a solution on this path 273 | // This position lead to dead-end, erase and try another 274 | state.erase(row,c); 275 | } 276 | // This is dead-end 277 | return false; 278 | } 279 | ``` -------------------------------------------------------------------------------- /manuscript/003_traversal_005_solutions.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Solutions 3 | 4 | ### Locked rooms 5 | 6 | Let's start with our goal. We want to determine whether we can visit all the locked rooms. However, this is a bit too complex, as we would need to consider both rooms and keys. We can simplify the problem by reformulating our goal: collect a complete set of keys. 7 | 8 | Because we are not concerned with the optimality of our solution, only whether it is possible to collect all keys, we can choose depth-first search as our base algorithm. We will use one key in each step of our solution. Using a key will potentially give us access to more keys. 9 | 10 | ![Example of one possible DFS execution on the example problem.](traversal/locked_rooms_dfs.png) 11 | 12 | Once we run out of keys, we can check whether we have collected a complete set. With a complete set of keys, we can visit all rooms. 13 | 14 | {caption: "Solution for the locked rooms problem."} 15 | ```cpp 16 | bool locked_rooms(const std::vector>& rooms) { 17 | // Keep track of the keys we have collected 18 | std::vector keys(rooms.size(),false); 19 | 20 | std::stack keys_to_use; 21 | keys_to_use.push(0); // We start with the key to room 0 22 | keys[0] = true; 23 | 24 | while (!keys_to_use.empty()) { 25 | // Use the key to open a room 26 | int key = keys_to_use.top(); 27 | keys_to_use.pop(); 28 | 29 | // Check if any of the keys in the room are new 30 | for (int k : rooms[key]) 31 | if (!keys[k]) { 32 | keys_to_use.push(k); 33 | keys[k] = true; 34 | } 35 | } 36 | 37 | // Do we have all the keys? 38 | return std::ranges::all_of(keys, std::identity{}); 39 | } 40 | ``` 41 | 42 | 43 | 44 | ### Bus routes 45 | 46 | We are trying to find the shortest path that minimizes the number of changes at bus stops. We could therefore use a breadth-first search and search across bus stops. 47 | 48 | However, that poses a problem. We don't have a convenient way to determine which bus stops we can reach. We could construct a data structure representing which bus stops can be reached by a single connection. However, such a data structure would grow based on the overall number of bus stops. 49 | 50 | We can do a lot better. Instead of considering bus stops, we can think in terms of bus lines. We still need to build a data structure that will provide the mapping of connections, i.e., for each bus line, list all other bus lines that we can switch to directly from this line. However, the big difference is that now the size of our data structure scales with the number of bus lines, not bus stops. 51 | 52 | To construct the bus line mapping, we can sort the list of bus stops for each line and then check each pair of buses for overlap. This leads to *O(s\*logs)* complexity for the sort and *O(b\*b\*s)* for the construction of the line mapping. 53 | 54 | Executing the breadth-first search on the bus line mapping will require *O(b\*b)* time. 55 | 56 | {caption: "Solution for the Bus routes problem."} 57 | ```cpp 58 | // There is no convenient is_overlapping algorithm unfortunately 59 | bool overlaps(const std::vector& left, const std::vector& right) { 60 | ptrdiff_t i = 0; ptrdiff_t j = 0; 61 | while (i < std::ssize(left) && j < std::ssize(right)) { 62 | while (i < std::ssize(left) && 63 | left[i] < right[j]) 64 | ++i; 65 | while (i < std::ssize(left) && 66 | j < std::ssize(right) && 67 | left[i] > right[j]) 68 | ++j; 69 | if (i < std::ssize(left) && 70 | j < std::ssize(right) && 71 | left[i] == right[j]) 72 | return true; 73 | } 74 | return false; 75 | } 76 | 77 | int min_tickets(std::vector> routes, int source, int target) { 78 | if (source == target) { return 0; } 79 | 80 | // Map of bus -> connecting busses 81 | std::vector> connections(routes.size()); 82 | for (auto &route : routes) 83 | std::ranges::sort(route); 84 | 85 | // Flag for whether a bus stops at target 86 | std::vector is_dest(routes.size(), false); 87 | // Flag for whether this bus was already visited 88 | std::vector visited(routes.size(), false); 89 | // Queue for BFS 90 | std::queue> q; 91 | 92 | for (ptrdiff_t i = 0; i < std::ssize(routes); ++i) { 93 | // The bus stops at source, one of our starting buses 94 | if (std::ranges::binary_search(routes[i], source)) { 95 | q.push({i,1}); 96 | visited[i] = true; 97 | } 98 | // The bus stops at target 99 | if (std::ranges::binary_search(routes[i], target)) 100 | is_dest[i] = true; 101 | 102 | // Find all other busses that connect to this bus 103 | for (ptrdiff_t j = i+1; j < std::ssize(routes); ++j) { 104 | if (overlaps(routes[i],routes[j])) { 105 | connections[i].push_back(j); 106 | connections[j].push_back(i); 107 | } 108 | } 109 | } 110 | 111 | // BFS 112 | while (!q.empty()) { 113 | auto [current,len] = q.front(); 114 | q.pop(); 115 | if (is_dest[current]) 116 | return len; 117 | 118 | for (auto bus : connections[current]) { 119 | if (visited[bus]) continue; 120 | q.push({bus, len+1}); 121 | visited[bus] = true; 122 | } 123 | } 124 | 125 | return -1; 126 | } 127 | ``` 128 | 129 | ### Counting islands 130 | 131 | Our first objective is to figure out a way to determine that a connected piece of land is an island. 132 | 133 | If we consider this problem from the perspective of traversing a piece of land, we will encounter not only the spaces this piece of land occupies but also all neighbouring spaces (otherwise, we could miss a piece of land). Therefore, we can reformulate this property. 134 | 135 | A piece of land is an island if we do not encounter the map boundary during our traversal. Encountering a land space extends this land mass, and encountering water maintains the island property. 136 | 137 | To ensure that we check all possible islands, we have to scan through the entire map, and when we encounter a space that hasn't been traversed yet, we start a new traversal to determine whether this land mass is an island. 138 | 139 | So far, I haven't specified whether we should use a depth-first or a breadth-first search. Unlike most other problems, where there is a clear preference towards one or the other, in this case, both end up equal in both time and space complexity. The example solution relies on a depth-first search. 140 | 141 | {caption: "Solution for the counting islands problem."} 142 | ```cpp 143 | // depth-first search 144 | bool island(int64_t i, int64_t j, std::vector>& grid) { 145 | // If we managed to reach out of bounds, this is not an island 146 | if (i == -1 || i == std::ssize(grid) || j == -1 || j == std::ssize(grid[i])) 147 | return false; 148 | // If this space is not land, ignore 149 | if (grid[i][j] != 'L') 150 | return true; 151 | // Mark this space as visited 152 | grid[i][j] = 'V'; 153 | 154 | // We can only return true (this is an island) if all four 155 | // directions of our DFS return true. However, at the same time 156 | // even if this is not an island we want to explore all spaces 157 | // of the land mass, just to mark it as visited. 158 | // If we used a boolean expression, we would run into 159 | // short-circuiting, the first "false" result would stop 160 | // the evaluation. 161 | // Here we take advantage of the bool->int conversion: 162 | // false == 0, true == 1 163 | return (island(i-1,j,grid) + island(i+1,j,grid) 164 | + island(i,j-1,grid) + island(i,j+1,grid)) == 4; 165 | } 166 | 167 | int count_islands(std::vector> grid) { 168 | int cnt = 0; 169 | // For every space 170 | for (int64_t i = 0; i < std::ssize(grid); ++i) 171 | for (int64_t j = 0; j < std::ssize(grid[i]); ++j) 172 | // If it is an unvisited land space, check if it is an island 173 | if (grid[i][j] == 'L' && island(i,j,grid)) 174 | ++cnt; 175 | return cnt; 176 | } 177 | ``` 178 | 179 | 180 | 181 | ### All valid parentheses sequences 182 | 183 | Enumerating all possible combinations under a specific constraint is a canonical problem for backtracking. Our first objective is to formulate our constraints. 184 | 185 | The first constraint follows from the input. Because we only have n pairs of parentheses, we can only add n left and n right parentheses. 186 | 187 | The second constraint encodes the validity of a parentheses sequence. Adding a left parenthesis will never produce an invalid sequence; however, each right parenthesis must match a previous left parenthesis, meaning we can never have more right parentheses than left parentheses. 188 | 189 | Finally, we need to make sure that we keep track of the values required to validate both constraints as we go, to avoid continually recounting the number of parentheses. 190 | 191 | {caption: "Solution for the valid parentheses problem."} 192 | ```cpp 193 | void generate(std::vector& solutions, size_t n, 194 | std::string& prefix, size_t left, size_t right) { 195 | // n parentheses, we have solution 196 | if (prefix.length() == 2*n) 197 | solutions.push_back(prefix); 198 | 199 | // We can only add a left parenthesis if we haven't used all of them. 200 | if (left < n) { 201 | prefix.push_back('('); 202 | // Explore all solutions with this prefix 203 | generate(solutions, n, prefix, left + 1, right); 204 | prefix.pop_back(); 205 | } 206 | // We can only add a left parenthesis if we have used more left 207 | // than right parentheses. 208 | if (left > right) { 209 | prefix.push_back(')'); 210 | // Explore all solutions with this prefix 211 | generate(solutions, n, prefix, left, right + 1); 212 | prefix.pop_back(); 213 | } 214 | } 215 | 216 | std::vector valid_parentheses(size_t n) { 217 | std::vector solutions; 218 | std::string prefix; 219 | generate(solutions, n, prefix, 0, 0); 220 | return solutions; 221 | } 222 | ``` 223 | 224 | ### Sudoku solver 225 | 226 | One of the requirements for a proper Sudoku puzzle is that it can be solved entirely without guessing simply by propagating the constraints. 227 | 228 | However, implementing a non-guessing Sudoku solver is not something you could do within a coding interview; therefore, we will need to limit our scope and do at least some guessing. At the same time, we do not want to completely brute force the puzzle, as that will be pretty slow. 229 | 230 | A good middle ground is applying the primary Sudoku constraint: each number can only appear once in each row, column, and box. Consequently, if we are guessing a number for a particular space, we can skip all the numbers already present in that row, column, and box. 231 | 232 | ![Example of the effect of primary Sudoku constraints. The highlighted cell has only two possible values: six and seven.](traversal/sudoku_constraints.png) 233 | 234 | The implementation mirrors the solution for the N-Queens problem with constraint propagation; however, because we are working with a statically sized puzzle (9x9), we can additionally take advantage of the fastest C++ containers: *std::array* and *std::bitset*. 235 | 236 | Each Sudoku puzzle has nine rows, nine columns, and nine boxes. Each of which we can represent with a *std::bitset*, where 1s represent digits already present in the corresponding row, column, or box. 237 | 238 | {caption: "Solution for the Sudoku solver problem."} 239 | ```cpp 240 | /* Calculate the corresponding box for row/col coordinates: 241 | 0 1 2 242 | 3 4 5 243 | 6 7 8 244 | 245 | Any mapping will work, as long as it is consistent. 246 | */ 247 | int64_t get_box(int64_t row, int64_t col) { 248 | return (row/3)*3+col/3; 249 | } 250 | 251 | struct State { 252 | // Initialize the state with given digits 253 | State(const std::vector>& puzzle) { 254 | for (int64_t i = 0; i < 9; ++i) 255 | for (int64_t j = 0; j < 9; ++j) 256 | if (puzzle[i][j] != ' ') 257 | mark(i, j, puzzle[i][j]-'1'); 258 | } 259 | 260 | std::array,9> row; 261 | std::array,9> col; 262 | std::array,9> box; 263 | 264 | // Get the already used digits for a specific space. 265 | std::bitset<9> used(int64_t r_idx, int64_t c_idx) { 266 | return row[r_idx] | col[c_idx] | box[get_box(r_idx,c_idx)]; 267 | } 268 | // Mark this digit as used in the corresponding row, column and box. 269 | void mark(int64_t r_idx, int64_t c_idx, int64_t digit) { 270 | row[r_idx][digit] = true; 271 | col[c_idx][digit] = true; 272 | box[get_box(r_idx, c_idx)][digit] = true; 273 | } 274 | // Mark this digit as unused in the corresponding row, column and box. 275 | void unmark(int64_t r_idx, int64_t c_idx, int64_t digit) { 276 | row[r_idx][digit] = false; 277 | col[c_idx][digit] = false; 278 | box[get_box(r_idx, c_idx)][digit] = false; 279 | } 280 | }; 281 | 282 | // Get the next empty space after {row,col} 283 | std::pair next( 284 | const std::vector>& puzzle, 285 | int64_t row, int64_t col) { 286 | int64_t start = col; 287 | for (int64_t i = row; i < std::ssize(puzzle); ++i) 288 | for (int64_t j = std::exchange(start,0); j < std::ssize(puzzle[i]); ++j) 289 | if (puzzle[i][j] == ' ') 290 | return {i,j}; 291 | return {-1,-1}; 292 | } 293 | 294 | bool backtrack( 295 | std::vector>& puzzle, 296 | State& state, 297 | int64_t r_curr, int64_t c_curr) { 298 | 299 | // next coordinate to fill 300 | auto [r_next, c_next] = next(puzzle, r_curr, c_curr); 301 | // {-1,-1} means there is no unfilled space, 302 | // i.e. we have solved the puzzle 303 | if (r_next == -1 && c_next == -1) 304 | return true; 305 | 306 | // The candidate numbers for this space cannot 307 | // repeat in the row, column or box. 308 | auto used = state.used(r_next, c_next); 309 | 310 | // Guess a number 311 | for (int64_t i = 0; i < 9; ++i) { 312 | // Already in a row, column or box 313 | if (used[i]) continue; 314 | 315 | // Mark it on the puzzle 316 | puzzle[r_next][c_next] = '1'+i; 317 | state.mark(r_next,c_next,i); 318 | 319 | if (backtrack(puzzle,state,r_next,c_next)) 320 | return true; 321 | // we get false if this was a guess 322 | // that didn't lead to a solution 323 | 324 | // Unmark from the puzzle 325 | state.unmark(r_next,c_next,i); 326 | puzzle[r_next][c_next] = ' '; 327 | // And try the next digit 328 | } 329 | return false; 330 | } 331 | 332 | bool solve(std::vector>& puzzle) { 333 | State state(puzzle); 334 | return backtrack(puzzle,state,0,0); 335 | } 336 | ``` 337 | 338 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | (c) RNDr. Šimon Tóth 2023 (business@simontoth.eu) 2 | 3 | # Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License 4 | 5 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 6 | 7 | ## Section 1 – Definitions. 8 | 9 |
    10 |
  1. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
  2. 11 |
  3. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
  4. 12 |
  5. BY-NC-SA Compatible License means a license listed at https://creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License.
  6. 13 |
  7. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
  8. 14 |
  9. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
  10. 15 |
  11. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
  12. 16 |
  13. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike.
  14. 17 |
  15. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
  16. 18 |
  17. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
  18. 19 |
  19. Licensor means the individual(s) or entity(ies) granting rights under this Public License.
  20. 20 |
  21. NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange.
  22. 21 |
  23. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
  24. 22 |
  25. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
  26. 23 |
  27. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
  28. 24 |
25 | 26 | ## Section 2 – Scope. 27 | 28 |
    29 |
  1. License grant.
      30 |
    1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
        31 |
      1. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and
      2. 32 |
      3. produce, reproduce, and Share Adapted Material for NonCommercial purposes only.
      4. 33 |
    2. 34 |
    3. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
    4. 35 |
    5. Term. The term of this Public License is specified in Section 6(a).
    6. 36 |
    7. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
    8. 37 |
    9. Downstream recipients.
        38 |
      1. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
      2. 39 |
      3. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply.
      4. 40 |
      5. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
      6. 41 |
    10. 42 |
    11. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
    12. 43 |
  2. 44 |
  3. Other rights.
      45 |
    1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
    2. 46 |
    3. Patent and trademark rights are not licensed under this Public License.
    4. 47 |
    5. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes.
    6. 48 |
  4. 49 |
50 | 51 | ## Section 3 – License Conditions. 52 | 53 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 54 | 55 |
    56 |
  1. Attribution.
      57 |
    1. If You Share the Licensed Material (including in modified form), You must:
        58 |
      1. retain the following if it is supplied by the Licensor with the Licensed Material:
          59 |
        1. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
        2. 60 |
        3. a copyright notice;
        4. 61 |
        5. a notice that refers to this Public License;
        6. 62 |
        7. a notice that refers to the disclaimer of warranties;
        8. 63 |
        9. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
        10. 64 |
      2. 65 |
      3. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
      4. 66 |
      5. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
      6. 67 |
    2. 68 |
    3. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
    4. 69 |
    5. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
    6. 70 |
  2. 71 |
  3. ShareAlike.
    72 | In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 73 |
      74 |
    1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License.
    2. 75 |
    3. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
    4. 76 |
    5. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
    6. 77 |
    78 |
79 | 80 | ## Section 4 – Sui Generis Database Rights. 81 | 82 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 83 | 84 |
    85 |
  1. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only;
  2. 86 |
  3. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
  4. 87 |
  5. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
  6. 88 |
89 | 90 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 91 | 92 | ## Section 5 – Disclaimer of Warranties and Limitation of Liability. 93 | 94 |
    95 |
  1. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
  2. 96 |
  3. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
  4. 97 |
  5. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
  6. 98 |
99 | 100 | ## Section 6 – Term and Termination. 101 | 102 |
    103 |
  1. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
  2. 104 |
  3. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
      105 |
    1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
    2. 106 |
    3. upon express reinstatement by the Licensor.

    4. 107 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
  4. 108 |
  5. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
  6. 109 |
  7. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
  8. 110 |
111 | 112 | ## Section 7 – Other Terms and Conditions. 113 | 114 |
    115 |
  1. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
  2. 116 |
  3. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
  4. 117 |
118 | 119 | ## Section 8 – Interpretation. 120 | 121 |
    122 |
  1. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
  2. 123 |
  3. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
  4. 124 |
  5. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
  6. 125 |
  7. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
  8. 126 |
127 | -------------------------------------------------------------------------------- /manuscript/004_trees_007_solutions.md: -------------------------------------------------------------------------------- 1 | {full: true, community: true} 2 | ## Solutions 3 | 4 | ### Serialize and de-serialize n-ary tree 5 | 6 | There are many possible approaches, as we can choose any format for serialization. However, all approaches should use a pre-order traversal since we need the parent node when we process its children. 7 | 8 | The following approach uses a recursive pre-order traversal and a "terminal value" format. The format terminates the list of children with the value -1, which is outside of the domain of uint32_t. For example, a single node tree with the root value of 0 will serialize into "0 -1". 9 | 10 | We have to choose how to handle an empty tree. We can either serialize an empty tree as "-1" or leave the output unchanged, representing an empty tree as an empty string. The former has the benefit of explicitly denoting an empty tree, allowing us to store a specific number of empty trees in serialized form (where an empty output is simply empty). 11 | 12 | To deserialize a node, we deserialize children until we read a negative value, processing the entire input using pre-order traversal. 13 | 14 | {caption: "Solution for serializing and de-serializing an n-ary tree."} 15 | ```cpp 16 | void serialize(Node* root, std::ostream& s) { 17 | // each node is serialized into "value {children} -1" 18 | s << root->value << " "; 19 | for (auto c : root->children) 20 | serialize(c, s); 21 | s << "-1 "; 22 | } 23 | 24 | std::ostream& operator<<(std::ostream& s, Tree& tree) { 25 | if (tree.root != nullptr) 26 | serialize(tree.root, s); 27 | else 28 | // serialize empty tree as "-1" 29 | s << "-1 "; 30 | return s; 31 | } 32 | 33 | Node* deserialize(Tree& tree, Node* parent, uint32_t parent_value, std::istream& s) { 34 | // pre-reading the value allows for cleaner code 35 | Node* result = tree.add_node(parent_value, parent); 36 | // we have to use int64_t to represent all values for uint32_t and -1 37 | int64_t value; 38 | while (s >> value) { 39 | if (value < 0) // if we read -1, we are done reading 40 | // the children of this node 41 | return result; 42 | // otherwise recursively de-serialize this child 43 | deserialize(tree, result, value, s); 44 | } 45 | return result; 46 | } 47 | 48 | std::istream& operator>>(std::istream& s, Tree& tree) { 49 | int64_t value; 50 | if (s >> value && value >= 0) 51 | deserialize(tree, nullptr, value, s); 52 | return s; 53 | } 54 | ``` 55 | 56 | 57 | 58 | ### Find all nodes of distance *k* in a binary tree 59 | 60 | One option is to translate the binary tree, essentially a directed graph, into an undirected graph, in which we can easily find k-distance nodes by applying a breadth-first search. 61 | 62 | However, we have a simpler option based on the following observation. Consider a node with one of its children. 63 | 64 | If the child doesn’t lie on the path between the node and our target, its distance to the target is simply one more than the distance of the parent node. If it does lie on the path between the node and the target, its distance is one less. 65 | 66 | {width: "35%"} 67 | ![Example that demonstrates the distance changing between children on the path to the target node or not.](trees/kdistance_distances.png) 68 | 69 | If we explore the graph using pre-order traversal, we will also have a second guarantee that a node is only on the path if it is also on the path between the root and the target node. 70 | 71 | Using these observations, we can construct a two-pass solution. 72 | 73 | First, we find our target and initialize the distances for all nodes on the path between the target and the tree's root. 74 | 75 | In the second pass, if we have a precomputed value for a node, we know that it is on the path, which allows us to distinguish between the two situations. Also, when we encounter a node with the appropriate distance, we remember it. 76 | 77 | {caption: "Solution"} 78 | ```cpp 79 | // Search for target and build distances to root 80 | int distance_search(Node* root, Node* target, 81 | std::unordered_map& distances) { 82 | if (root == nullptr) 83 | return -1; 84 | if (root == target) { 85 | distances[root->value] = 0; 86 | return 0; 87 | } 88 | // Target in the left sub-tree 89 | if (int left = distance_search(root->left, target, distances); 90 | left >= 0) { 91 | distances[root->value] = left + 1; 92 | return left + 1; 93 | } 94 | // Target in the right sub-tree 95 | if (int right = distance_search(root->right, target, distances); 96 | right >= 0) { 97 | distances[root->value] = right + 1; 98 | return right + 1; 99 | } 100 | // Target not in this sub-tree 101 | return -1; 102 | } 103 | 104 | // Second pass traversal. 105 | void dfs(Node* root, Node* target, int k, int dist, 106 | std::unordered_map& distances, 107 | std::vector& result) { 108 | if (root == nullptr) return; 109 | // Check if this node is on the path to target. 110 | auto it = distances.find(root->value); 111 | // Node is on the path to target, update distance. 112 | if (it != distances.end()) 113 | dist = it->second; 114 | // This node is k distance from the target. 115 | if (dist == k) 116 | result.push_back(root->value); 117 | 118 | // Distances to children are one more, unless they are on the path 119 | // which is handled above. 120 | dfs(root->left, target, k, dist + 1, distances, result); 121 | dfs(root->right, target, k, dist + 1, distances, result); 122 | } 123 | 124 | std::vector find_distance_k_nodes(Node* root, Node* target, int k) { 125 | // First pass 126 | std::unordered_map distances; 127 | distance_search(root, target, distances); 128 | // Second pass 129 | std::vector result; 130 | dfs(root, target, k, distances[root->value], distances, result); 131 | return result; 132 | } 133 | ``` 134 | 135 | 136 | 137 | ### Sum of distances to all nodes 138 | 139 | We will start with a simpler sub-problem: calculate the sum of distances to all nodes for the root node only. 140 | 141 | Let's consider a node with a child subtree represented by the left child. When we move from the child to the parent, we increase the distance to all nodes in this subtree by one or put another way, we increase the total distance by *node_count(left_subtree)*. 142 | 143 | Therefore, if we want to calculate the sum of distances for the root node, we can do a post-order traversal. At each node, we calculate the sum of distances as *sum_of_distances(left) + node_count(left) + sum_of_distances(right) + node_count(right)*. 144 | 145 | Because this approach only gives us the solution to the root node, applying this process to the entire tree would require rotating the tree, but more importantly, it would lead to *O(n\*n)* complexity. 146 | 147 | Fortunately, we can do better. 148 | 149 | Instead of focusing on the nodes, let's focus on the edges between them. Let's consider a specific edge that we have removed from the tree, and we have calculated the sum of distances for the two nodes originally connected by the removed edge. 150 | 151 | {width: "20%"} 152 | ![Example of a disconnected tree with the two highlighted nodes for which we have calculated the sum of distances values.](trees/sum_structure.png) 153 | 154 | We can reconstruct the sum of distances for the connected tree from the two disjoint values. 155 | 156 | - *sum_of_distances(a) = disconnected_sum(a) + disconnected_sum(b) + node_count(b)* 157 | - *sum_of_distances(b) = disconnected_sum(b) + disconnected_sum(a) + node_count(a)* 158 | - *sum_of_distances(a) - sum_of_distances(b) = node_count(b) - node_count(a)* 159 | 160 | This formula gives us the opportunity to calculate the answer for a child from the value of a parent. 161 | 162 | - *sum_of_distances(child) = sum_of_distances(parent) + node_count(parent) - node_count(child)* 163 | - *sum_of_distances(child) = sum_of_distances(parent) + (total_nodes - node_count(child)) - node_count(child)* 164 | - *sum_of_distances(child) = sum_of_distances(parent) + total_node - 2\*node_count(child)* 165 | 166 | After we have calculated the sum of distances for the root node with post-order traversal, we do a second traversal, this time in pre-order, filling in values for all nodes using the above formula. 167 | 168 | This gives us a much better *O(n)* time complexity. 169 | 170 | {caption: "Solution for the sum of distances problem."} 171 | ```cpp 172 | struct TreeInfo { 173 | TreeInfo(int n) : subtree_sum(n,0), node_count(n,0), result(n,0) {} 174 | std::vector subtree_sum; 175 | std::vector node_count; 176 | std::vector result; 177 | }; 178 | 179 | void post_order(int node, int parent, 180 | const std::unordered_multimap& neighbours, TreeInfo& info) { 181 | // If there are no children we have zero distance and one node. 182 | info.subtree_sum[node] = 0; 183 | info.node_count[node] = 1; 184 | 185 | auto [begin, end] = neighbours.equal_range(node); 186 | for (auto [from, to] : std::ranges::subrange(begin, end)) { 187 | // Avoid looping back to the node we came from. 188 | if (to == parent) continue; 189 | // post_order traversal, visit children first 190 | post_order(to, node, neighbours, info); 191 | // accumulate number of nodes and distances 192 | info.subtree_sum[node] += info.subtree_sum[to] + info.node_count[to]; 193 | info.node_count[node] += info.node_count[to]; 194 | } 195 | } 196 | 197 | void pre_order(int node, int parent, 198 | const std::unordered_multimap& neighbours, TreeInfo& info) { 199 | // For the root node the subtree_sum matches the result. 200 | if (parent == -1) { 201 | info.result[node] = info.subtree_sum[node]; 202 | } else { 203 | // Otherwise, we can calculate the result from the parent, 204 | // because in pre-order we visit the parent before the children. 205 | info.result[node] = info.result[parent] + info.result.size() 206 | - 2*info.node_count[node]; 207 | } 208 | // Now visit any children. 209 | auto [begin, end] = neighbours.equal_range(node); 210 | for (auto [from, to] : std::ranges::subrange(begin, end)) { 211 | if (to == parent) continue; 212 | pre_order(to, node, neighbours, info); 213 | } 214 | } 215 | 216 | std::vector distances_in_tree( 217 | int n, const std::unordered_multimap neighbours) { 218 | TreeInfo info(n); 219 | // post-order pass to calculate subtree_sum and node_count 220 | post_order(0,-1,neighbours,info); 221 | // pre-order pass to calculate result 222 | pre_order(0,-1,neighbours,info); 223 | return info.result; 224 | } 225 | ``` 226 | 227 | 228 | 229 | ### Well-behaved paths in a tree 230 | 231 | This is a reasonably tricky problem. If the problem was limited to binary trees, we could use post-order traversal, keep track of values not blocked by parent nodes with higher values and then construct intersections from the left and right subtrees. 232 | 233 | We can't apply this simple logic to an n-arry tree as we would have to check every subtree against every other subtree at each node, quickly exploding the complexity. 234 | 235 | Instead of considering the nodes, let's think in terms of edges. Because we are working with a tree, each edge divides the tree into two trees. 236 | 237 | As a reminder, a valid path requires that both ends have the same values and all intermediate nodes are, at most, equal to the ends of the path. This gives us a hint towards using an ordered approach. 238 | 239 | Let's start with an empty tree with no edge and slowly add the edges in non-descending order based on the values of the nodes on their ends, i.e. *std::max(value[node_left], value[node_right])*. 240 | 241 | This gives us some interesting properties: 242 | 243 | - The maximum value in either of the trees we are connecting by adding this edge is at most *std::max(value[node_left], value[node_right])*, and that has to be the maximum value in at least one of the trees (because both nodes already exist in those trees). 244 | - If the maximum value in one of the subtrees is lower than *std::max(value[node_left], value[node_right])*, no valid paths are crossing this edge (since the maximum node creates a barrier). 245 | - If the maximum value in both of the subtrees is *std::max(value[node_left], value[node_right])*, then this edge adds *freq_of_max[left]\*freq_of_max[right]* valid paths. From each node node with the maximum value in the left subtree to each node with the maximum value in the right subtree. 246 | 247 | While this looks like a complete solution, we have a big problem. How do we efficiently keep track of the frequencies of the maximum values? Not only that, a new edge might be connecting to any node in a connected subtree, so we also need to be able to retrieve the frequency for a connected subtree based on any node in this subtree. 248 | 249 | Fortunately, the UnionFind algorithm offers a solution. Union find can keep track of connected components by keeping track of a representative node for each component. In our case, the components are subtree, and we additionally want the representative node to be one of the nodes with the maximum value. 250 | 251 | {caption: "Solution for the well-behaved paths problem."} 252 | ```cpp 253 | // UnionFind 254 | int64_t find(std::vector& root, int64_t i) { 255 | if (root[i] == i) // This is the representative node for this subtree 256 | return i; 257 | // Otherwise find the representative node and cache for future lookups. 258 | // The caching is what provides the O(alpha(n)) complexity. 259 | return root[i] = find(root, root[i]); 260 | } 261 | 262 | int64_t well_behaved_paths(std::vector values, 263 | std::vector> edges) { 264 | // Start with all nodes disconnected, each node is the 265 | // representative node of its subtree. 266 | std::vector root(values.size(), 0); 267 | std::ranges::iota(root,0); 268 | 269 | // The frequencies of the maximum value in each subtree. 270 | std::vector freq(values.size(), 1); 271 | 272 | // Start with trivial paths. 273 | int64_t result = values.size(); 274 | 275 | std::ranges::sort(edges, [&](auto& l, auto& r) { 276 | return std::max(values[l.first], values[l.second]) < 277 | std::max(values[r.first], values[r.second]); 278 | }); 279 | 280 | for (auto &edge : edges) { 281 | // Find the representative nodes for the two ends. 282 | // The representative nodes are always the maximum value nodes. 283 | int64_t l_max = find(root, edge.first); 284 | int64_t r_max = find(root, edge.second); 285 | 286 | // The maximum in both subtrees is the same. 287 | if (values[l_max] == values[r_max]) { 288 | // Add path from each maximum node in left subtree to each 289 | // maximum node in right subtree. 290 | result += freq[l_max]*freq[r_max]; 291 | 292 | // Merge the trees right into left 293 | freq[l_max] += freq[r_max]; 294 | root[r_max] = l_max; 295 | } else if (values[l_max] > values[r_max]) { 296 | // No new paths, but merge the trees 297 | // right into left. 298 | root[r_max] = l_max; 299 | // This doesn't change the frequency because 300 | // all nodes in r_max subtree < values[l_max]. 301 | } else { // (values[r_max] > values[l_max]) 302 | // No new paths, but merge the trees 303 | // left into right. 304 | root[l_max] = r_max; 305 | // This doesn't change the frequency because 306 | // all nodes in l_max subtree < values[r_max]. 307 | } 308 | } 309 | return result; 310 | } 311 | ``` 312 | 313 | 314 | 315 | ### Number of reorders of a serialized BST 316 | 317 | First, let’s remind ourselves how Binary Search trees operate. For each node, all the nodes in the left subtree are lower than the value of this node, and all nodes in the right subtree are higher than the value of this node. 318 | 319 | This allows us to find any node with a specific value in *log(n)*. 320 | 321 | Determining the number of reorderings that produce the same BST is much less obvious. However, the first fairly straightforward observation is that the first node will always be the root node, and it creates a partition over the other nodes (for the left and right subtrees). 322 | 323 | This points to the first part contributing to the total count of reorderings. Any reordering of elements that produces the same stable partition will lead to the same BST. 324 | 325 | Let’s consider the permutation *{3,1,2,4,5}*. Changing the order of elements within each partition (i.e. *{1,2}*, *{4,5}*) would produce different partitions; however, we can freely interleave these partitions without changing the result. More formally, we are looking for the number of ways to pick the positions for the left (or right) partition out of all positions, i.e. *C(n-1,k)* (binomial coefficient), where n is the total number of elements in the permutation and *k* is the number of elements in the left partition. 326 | 327 | The second point we have not considered is the number of reorderings in the two sub-tree, which we can calculate recursively. 328 | 329 | This leads to a total formula: *reorderings(left)\*reorderings(right)\*coeff(n-1,left.size())*. 330 | 331 | This implies that we will have to pre-calculate the binomial coefficients, which we can do using Pascal’s triangle. 332 | 333 | Finally, we need to apply the requested modulo operation where appropriate. 334 | 335 | {caption: "Solution for the number of BST reorders problem."} 336 | ```cpp 337 | constexpr inline int32_t mod = 1e9+7; 338 | 339 | int32_t count(std::span nums, 340 | std::vector>& coef) { 341 | if (nums.size() < 3) return 1; 342 | 343 | // Partition into left and right child 344 | auto rng = std::ranges::stable_partition(nums, 345 | [pivot = nums[0]](int v) { return v < pivot; }); 346 | auto left = std::span(nums.begin(), rng.begin()); 347 | // Skip the pivot, since the pivot is the parent node 348 | auto right = std::span(rng.begin()+1, nums.end()); 349 | 350 | // Calculate the number of reorders for both sub-trees 351 | int64_t left_cnt = count(left, coef) % mod; 352 | int64_t right_cnt = count(right, coef) % mod; 353 | // Side note, we need 64bit here because we need 354 | // to fit 32bit*32bit in the bellow calculation. 355 | 356 | // The result is: 357 | // left * right * number of ways to pick positions 358 | // for left.size() elements in nums.size()-1 positions. 359 | return ((left_cnt*right_cnt) % mod) 360 | *coef[nums.size()-1][left.size()] % mod; 361 | } 362 | 363 | int32_t number_of_reorders(std::span nums) { 364 | // Precalculate binomial coefficients upto nums.size()-1 365 | std::vector> binomial_coefficients; 366 | binomial_coefficients.resize(nums.size()); 367 | for (int64_t i = 0; i < std::ssize(nums); ++i) { 368 | binomial_coefficients[i].resize(i+1, 1); 369 | for (int64_t j = 1; j < i; ++j) 370 | // Pascal's triangle 371 | binomial_coefficients[i][j] = 372 | (binomial_coefficients[i-1][j-1] + 373 | binomial_coefficients[i-1][j]) % mod; 374 | } 375 | 376 | return (count(nums, binomial_coefficients)-1) % mod; 377 | } 378 | ``` 379 | 380 | --------------------------------------------------------------------------------