diff --git a/DIRECTORY.md b/DIRECTORY.md index 491f4d62..88c99bea 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -27,6 +27,8 @@ * [Test Decode Message](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/decode_message/test_decode_message.py) * Letter Combination * [Test Letter Combination](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/letter_combination/test_letter_combination.py) + * Letter Tile Possibilities + * [Test Num Tile Possibilities](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/letter_tile_possibilities/test_num_tile_possibilities.py) * Optimal Account Balancing * [Test Optimal Account Balancing](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/optimal_account_balancing/test_optimal_account_balancing.py) * Partition String @@ -69,6 +71,8 @@ * [Test Domino Tromino Tiling](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/domino_tromino_tiling/test_domino_tromino_tiling.py) * Duffle Bug Value * [Test Max Duffle Bag Value](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/duffle_bug_value/test_max_duffle_bag_value.py) + * Frog Jump + * [Test Frog Jump](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/frog_jump/test_frog_jump.py) * House Robber * [Test House Robber](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/house_robber/test_house_robber.py) * Interleaving String @@ -79,6 +83,13 @@ * [Test Longest Common Subsequence](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/longest_common_subsequence/test_longest_common_subsequence.py) * Longest Increasing Subsequence * [Test Longest Increasing Subsequence](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/longest_increasing_subsequence/test_longest_increasing_subsequence.py) + * Max Profit In Job Scheduling + * [Test Job Scheduling](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/max_profit_in_job_scheduling/test_job_scheduling.py) + * Max Subarray + * [Sub Array Maxsum](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/max_subarray/sub_array_maxsum.py) + * [Test Max Subarray](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/max_subarray/test_max_subarray.py) + * Maximal Rectangle + * [Test Maximal Rectangle](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/maximal_rectangle/test_maximal_rectangle.py) * Maximal Square * [Test Maximal Square](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/maximal_square/test_maximal_square.py) * Min Distance @@ -127,6 +138,8 @@ * [Test Min Cost To Supply](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/min_cost_to_supply/test_min_cost_to_supply.py) * Nearest Exit From Entrance In Maze * [Test Nearest Exit From Entrance](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/nearest_exit_from_entrance_in_maze/test_nearest_exit_from_entrance.py) + * Network Delay Time + * [Test Network Delay Time](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/network_delay_time/test_network_delay_time.py) * Number Of Islands * [Test Number Of Islands](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_islands/test_number_of_islands.py) * [Union Find](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_islands/union_find.py) @@ -140,13 +153,19 @@ * [Test Reorder Routes](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/reorder_routes/test_reorder_routes.py) * Rotting Oranges * [Test Rotting Oranges](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/rotting_oranges/test_rotting_oranges.py) + * Single Cycle Check + * [Test Single Cycle Check](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/single_cycle_check/test_single_cycle_check.py) * Valid Path * [Test Valid Path](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/valid_path/test_valid_path.py) + * Valid Tree + * [Test Graph Valid Tree](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/valid_tree/test_graph_valid_tree.py) * Greedy * Assign Cookies * [Test Assign Cookies](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/greedy/assign_cookies/test_assign_cookies.py) * Boats * [Test Boats To Save People](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/greedy/boats/test_boats_to_save_people.py) + * Create Maximum Number + * [Test Create Maximum Number](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/greedy/create_maximum_number/test_create_maximum_number.py) * Gas Stations * [Test Gas Stations](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/greedy/gas_stations/test_gas_stations.py) * Jump Game @@ -220,6 +239,8 @@ * [Test Min Meeting Rooms](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/meeting_rooms/test_min_meeting_rooms.py) * Merge Intervals * [Test Merge Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/merge_intervals/test_merge_intervals.py) + * Non Overlapping Intervals + * [Test Non Overlapping Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/non_overlapping_intervals/test_non_overlapping_intervals.py) * Remove Intervals * [Test Remove Covered Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/remove_intervals/test_remove_covered_intervals.py) * Task Scheduler @@ -250,18 +271,23 @@ * Search Suggestions * [Test Search Suggestions](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/search/trie/search_suggestions/test_search_suggestions.py) * Sliding Window + * Length Of Longest Substring + * [Test Length Of Longest Substring](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/sliding_window/length_of_longest_substring/test_length_of_longest_substring.py) + * [Test Longest Substring Without Repeating Characters](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/sliding_window/length_of_longest_substring/test_longest_substring_without_repeating_characters.py) * Longest Repeating Char Replacement * [Test Longest Repeating Character Replacement](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/sliding_window/longest_repeating_char_replacement/test_longest_repeating_character_replacement.py) * Longest Substring With K Repeating Chars * [Test Longest Substring K Repeating Chars](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/sliding_window/longest_substring_with_k_repeating_chars/test_longest_substring_k_repeating_chars.py) - * Longest Substring Without Repeating Characters - * [Test Longest Substring Without Repeating Characters](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/sliding_window/longest_substring_without_repeating_characters/test_longest_substring_without_repeating_characters.py) * Max Points From Cards * [Test Max Points From Cards](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/sliding_window/max_points_from_cards/test_max_points_from_cards.py) * Max Sum Of Subarray * [Test Max Sum Sub Array](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/sliding_window/max_sum_of_subarray/test_max_sum_sub_array.py) + * Minimum Window Substring + * [Test Min Window Substring](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/sliding_window/minimum_window_substring/test_min_window_substring.py) * Repeated Dna Sequences * [Test Repeated Dna Sequences](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/sliding_window/repeated_dna_sequences/test_repeated_dna_sequences.py) + * Substring Concatenation + * [Test Substring With Concatenation Of Words](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/sliding_window/substring_concatenation/test_substring_with_concatenation_of_words.py) * Sorting * Heapsort * [Test Heap Sort](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/sorting/heapsort/test_heap_sort.py) @@ -288,6 +314,8 @@ * Two Pointers * Array 3 Pointers * [Test Array 3 Pointers](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/array_3_pointers/test_array_3_pointers.py) + * Container With Most Water + * [Test Container With Most Water](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/container_with_most_water/test_container_with_most_water.py) * Count Pairs * [Test Count Pairs](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/count_pairs/test_count_pairs.py) * Find Sum Of Three @@ -317,6 +345,8 @@ * [Test Reverse Array](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/reverse_array/test_reverse_array.py) * Reverse Words * [Test Reverse Words](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/reverse_words/test_reverse_words.py) + * Sort By Parity + * [Test Sort By Parity](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/sort_by_parity/test_sort_by_parity.py) * Sort Colors * [Test Sort Colors](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/sort_colors/test_sort_colors.py) * Three Sum @@ -327,6 +357,8 @@ * [Test Two Sum](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/two_sum/test_two_sum.py) * Two Sum Less K * [Test Two Sum](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/two_sum_less_k/test_two_sum.py) + * Valid Word Abbreviation + * [Test Valid Word Abbreviation](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/two_pointers/valid_word_abbreviation/test_valid_word_abbreviation.py) * Unique Bsts * [Unique Bsts](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/unique_bsts/unique_bsts.py) * Word Count @@ -394,12 +426,8 @@ * Matrix * Settozero * [Test Set Matrix Zero](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/matrix/settozero/test_set_matrix_zero.py) - * Max Subarray - * [Sub Array Maxsum](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/max_subarray/sub_array_maxsum.py) * Minincrementsforunique * [Min Increment For Unique](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/minincrementsforunique/min_increment_for_unique.py) - * Non Overlapping Intervals - * [Test Non Overlapping Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/non_overlapping_intervals/test_non_overlapping_intervals.py) * Sub Array With Sum * [Test Sub Array With Sum](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/sub_array_with_sum/test_sub_array_with_sum.py) * Subarrays With Fixed Bounds @@ -446,6 +474,7 @@ * [Test Singly Linked List Remove Duplicates](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/test_singly_linked_list_remove_duplicates.py) * [Test Singly Linked List Remove Nth Last Node](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/test_singly_linked_list_remove_nth_last_node.py) * [Test Singly Linked List Reorder List](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/test_singly_linked_list_reorder_list.py) + * [Test Singly Linked List Reverse K Group](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/test_singly_linked_list_reverse_k_group.py) * [Test Singly Linked List Rotate](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/test_singly_linked_list_rotate.py) * [Test Singly Linked Merge And Weave](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/test_singly_linked_merge_and_weave.py) * [Test Singly Linked Move Tail To Head](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/test_singly_linked_move_tail_to_head.py) @@ -506,21 +535,24 @@ * Search Tree * Avl * [Node](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/search_tree/avl/node.py) + * [Binary Search Tree](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/search_tree/binary_search_tree.py) * [Bst Iterator](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/search_tree/bst_iterator.py) + * [Bst Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/search_tree/bst_utils.py) * [Test Binary Search Tree Delete Node](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/search_tree/test_binary_search_tree_delete_node.py) * [Test Binary Search Tree Inorder Successor](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/search_tree/test_binary_search_tree_inorder_successor.py) * [Test Binary Search Tree Insert](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/search_tree/test_binary_search_tree_insert.py) * [Test Binary Search Tree Search](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/search_tree/test_binary_search_tree_search.py) + * [Test Binary Search Tree Valid](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/search_tree/test_binary_search_tree_valid.py) * [Test Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/test_utils.py) * Tree * [Binary Tree](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/binary_tree.py) + * [Binary Tree Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/binary_tree_utils.py) * [Test Binary Tree](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree.py) * [Test Binary Tree Deserialize](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_deserialize.py) * [Test Binary Tree Invert Tree](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_invert_tree.py) * [Test Binary Tree Min Camera Cover](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_min_camera_cover.py) * [Test Binary Tree Serialize](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_serialize.py) * [Test Binary Tree Visible Nodes](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_visible_nodes.py) - * [Tree Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/tree_utils.py) * [Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/utils.py) * Btree * [Node](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/btree/node.py) @@ -695,8 +727,6 @@ * [Test Can Place Flowers](https://github.com/BrianLusina/PythonSnips/blob/master/puzzles/arrays/can_place_flowers/test_can_place_flowers.py) * Candy * [Test Candy](https://github.com/BrianLusina/PythonSnips/blob/master/puzzles/arrays/candy/test_candy.py) - * Container With Most Water - * [Test Container With Most Water](https://github.com/BrianLusina/PythonSnips/blob/master/puzzles/arrays/container_with_most_water/test_container_with_most_water.py) * H Index * [Test H Index](https://github.com/BrianLusina/PythonSnips/blob/master/puzzles/arrays/h_index/test_h_index.py) * Increasing Triplet Subsequence @@ -874,6 +904,10 @@ * [Test Perfect Squares](https://github.com/BrianLusina/PythonSnips/blob/master/pymath/perfect_square/test_perfect_squares.py) * Rectangle Area * [Test Compute Area](https://github.com/BrianLusina/PythonSnips/blob/master/pymath/rectangle_area/test_compute_area.py) + * Reverse Integer + * [Test Reverse Integer](https://github.com/BrianLusina/PythonSnips/blob/master/pymath/reverse_integer/test_reverse_integer.py) + * Self Crossing + * [Test Is Self Crossing](https://github.com/BrianLusina/PythonSnips/blob/master/pymath/self_crossing/test_is_self_crossing.py) * Super Size * [Test Super Size](https://github.com/BrianLusina/PythonSnips/blob/master/pymath/super_size/test_super_size.py) * Triangles @@ -972,8 +1006,6 @@ * Bfs * Graphs * [Test Find If Path Exists](https://github.com/BrianLusina/PythonSnips/blob/master/tests/algorithms/bfs/graphs/test_find_if_path_exists.py) - * Sliding Window - * [Test Min Window Substring](https://github.com/BrianLusina/PythonSnips/blob/master/tests/algorithms/sliding_window/test_min_window_substring.py) * Sorting * [Test Counting Sort](https://github.com/BrianLusina/PythonSnips/blob/master/tests/algorithms/sorting/test_counting_sort.py) * Strings @@ -1039,7 +1071,6 @@ * [Test Highest Rank](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_highest_rank.py) * [Test Lonely Integer](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_lonely_integer.py) * [Test Longest Consecutive Sequence](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_longest_consecutive_sequence.py) - * [Test Max Subarray](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_max_subarray.py) * [Test Zig Zag Sequence](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_zig_zag_sequence.py) * [Test Build One 2 3](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/test_build_one_2_3.py) * [Test Consecutive](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/test_consecutive.py) diff --git a/algorithms/backtracking/letter_tile_possibilities/README.md b/algorithms/backtracking/letter_tile_possibilities/README.md new file mode 100644 index 00000000..6457e1ac --- /dev/null +++ b/algorithms/backtracking/letter_tile_possibilities/README.md @@ -0,0 +1,144 @@ +# Letter Tile Possibilities + +You are given a string, tiles, consisting of uppercase English letters. You can arrange the tiles into sequences of any +length (from 1 to the length of tiles), and each sequence must include at most one tile, tiles[i], from tiles. + +Your task is to return the number of possible non-empty unique sequences you can make using the letters represented on +tiles[i]. + +## Constraints: + +- 1 ≤ `tiles.length` ≤ 7 +- The `tiles` string consists of uppercase English letters. + +## Examples + +Example 1: + +```text +Input: tiles = "AAB" +Output: 8 +Explanation: The possible sequences are "A", "B", "AA", "AB", "BA", "AAB", "ABA", "BAA". +``` + +Example 2: +```text +Input: tiles = "AAABBC" +Output: 188 +``` + +Example 3: +```text +Input: tiles = "V" +Output: 1 +``` + +## Topics + +- Hash Table +- String +- Backtracking +- Counting + +## Solutions + +### Permutations and Combinations + +This problem would have been straightforward if the tiles had no duplicates and we only needed to find sequences of the +same length as the given tiles. In that case, we could simply calculate the total unique sequences using n! (where n is +the length of the tiles). + +![Sample](./images/examples/letter_tile_possibilities_example_sample.png) + +However, in this problem, we need to find all unique sequences of any length—from 1 to the full length of the tiles—while +accounting for duplicate tiles. To achieve this, we take the following approach: + +We begin by sorting the letters in tiles so that similar letters are grouped. This allows us to systematically explore +possible letter sets without repeating unnecessary work. + +To generate sequences of lengths ranging from 1 to n, we consider each letter one by one. We start with a single +character, determine its possible sequences, then move to two-character combinations, find their possible sequences, +and so on. For each new character, we have two choices: either include the current letter in our selection or skip it +and move to the next one. By exploring both choices, we generate all possible letter sets. + +To prevent counting the same sequences multiple times due to duplicate letters, we ensure (in the previous step) that +duplicate letter sets are not counted more than once while generating different letter sets. Then, for each set, we +calculate how many distinct sequences can be formed by applying the updated formula for permutations, which accounts +for repeated letters: `n! / c!`, where c is the count of each repeated letter. + +The more repeated letters in a set, the fewer unique ways it can be arranged. + +We repeat this process for every possible letter set. Eventually, we sum the count of all these possible sequences to +obtain the total number of unique non-empty sequences. Finally, we subtract one from our final count to exclude the +empty set, as it does not contribute to valid arrangements. + +Let’s look at the following illustration to understand this better. + +![Solution 0](./images/solutions/letter_tile_possibilities_solution_0.png) + +Now, let’s look at the algorithm steps of this solution: + +- Initialize a set, unique_letter_sets, to track unique letter sets. +- Sort the input string, tiles, to group identical letters together. +- Generate unique letter sets and count their permutations: + - The function generate_sequences is used to recursively generate all possible letter subsets. + - We calculate the number of valid sequences using `count_permutations` for each unique letter set. The + `count_permutations` uses the helper function `factorial(n)`, which computes the factorial of a given number. +- The recursive function, `generate_sequences(tiles, current_letter_set, index, unique_letter_sets)`, works as follows: + - **Base case**: If `index` reaches the end of tiles, check if `current_letter_set` is unique. If it is: + - Add it to `unique_letter_sets`. + - Compute its permutations and return the count. + - **Recursive cases**: + - Exclude the current character (move to the next index). Store its result in the variable `without_letter`. + - Include the current character (append it and move to the next index). Store its result in the variable `with_letter`. + - The sum of both recursive calls gives the total count of valid sequences. + - Once all the letters in tiles have been explored, return the output. As an empty set is also considered in the + recursive process, we subtract 1 before returning the output. + +![Solution 1](./images/solutions/letter_tile_possibilities_solution_1.png) +![Solution 2](./images/solutions/letter_tile_possibilities_solution_2.png) +![Solution 3](./images/solutions/letter_tile_possibilities_solution_3.png) +![Solution 4](./images/solutions/letter_tile_possibilities_solution_4.png) +![Solution 5](./images/solutions/letter_tile_possibilities_solution_5.png) +![Solution 6](./images/solutions/letter_tile_possibilities_solution_6.png) +![Solution 7](./images/solutions/letter_tile_possibilities_solution_7.png) +![Solution 8](./images/solutions/letter_tile_possibilities_solution_8.png) +![Solution 9](./images/solutions/letter_tile_possibilities_solution_9.png) +![Solution 10](./images/solutions/letter_tile_possibilities_solution_10.png) +![Solution 11](./images/solutions/letter_tile_possibilities_solution_11.png) +![Solution 12](./images/solutions/letter_tile_possibilities_solution_12.png) +![Solution 13](./images/solutions/letter_tile_possibilities_solution_13.png) +![Solution 14](./images/solutions/letter_tile_possibilities_solution_14.png) +![Solution 15](./images/solutions/letter_tile_possibilities_solution_15.png) +![Solution 16](./images/solutions/letter_tile_possibilities_solution_16.png) +![Solution 17](./images/solutions/letter_tile_possibilities_solution_17.png) +![Solution 18](./images/solutions/letter_tile_possibilities_solution_18.png) +![Solution 19](./images/solutions/letter_tile_possibilities_solution_19.png) +![Solution 10](./images/solutions/letter_tile_possibilities_solution_20.png) +![Solution 11](./images/solutions/letter_tile_possibilities_solution_21.png) + +#### Time Complexity + +Let’s break down and analyze the time complexity of this solution: +- Sorting the input string takes `O(n log(n))` time, where n is the length of the tiles. +- The recursive function explores all possible subsets of the given tiles. As each tile can either be included or + skipped, there are 2^n subsets in the worst case. +- For each subset, we calculate the number of unique sequences that can be formed using the formula that accounts for + duplicate letters. + - Computing the factorial and handling character frequencies takes at most `O(n)` time per subset. + - As we do this for all 2^n subsets, the total time complexity is `O(2 ^n × n)`, which dominates the sorting step. + +If we sum these up, the overall time complexity simplifies to: + +`O(nlogn) + O(2^n) + O(2^n×n) = O(2^n×n)` + +#### Space Complexity +Let’s analyze the space complexity of this solution: +- As the recursion goes as deep as the length of the string n, the worst case space used by the function calls is `O(n)`. +- We store all unique subsets of tiles in a set, which can hold up to O(2^n) subsets in the worst case. Each sequence in + the set can be up to length n. So, the set uses `O(2^n × n)` space. +- The permutation calculation uses extra space, but this is at most O(n). + +If we add these up, the overall space complexity simplifies to: + +`O(n) + O(2^n × n) + O(n) = O(2^n × n)` diff --git a/algorithms/backtracking/letter_tile_possibilities/__init__.py b/algorithms/backtracking/letter_tile_possibilities/__init__.py new file mode 100644 index 00000000..aeb7196b --- /dev/null +++ b/algorithms/backtracking/letter_tile_possibilities/__init__.py @@ -0,0 +1,94 @@ +from typing import List, Set +from itertools import permutations + + +def num_tile_possibilities(tiles: str) -> int: + # Store unique sequences to avoid duplicates + unique_letter_sets: Set[str] = set() + + # Sort characters to handle duplicates efficiently + sorted_tiles: str = "".join(sorted(tiles)) + + def generate_sequences(current: str, index: int): + # Recursively generates all possible sequences by including/excluding each tile. + # Once a new sequence is found, its unique permutations are counted. + if index >= len(sorted_tiles): + # If a new sequence is found, count its unique permutations + if current not in unique_letter_sets: + unique_letter_sets.add(current) + + total_permutations = len(set(permutations(current))) + return total_permutations + return 0 + + # Explore two possibilities: skipping the current character or including it + without_letter = generate_sequences(current, index=index + 1) + with_letter = generate_sequences(current + sorted_tiles[index], index=index + 1) + + # Return all the possible sequences + return without_letter + with_letter + + # Optionally, you can write the count_permutations and the factorial method if not using builtin methods + # def factorial(n): + # + # # Computes the factorial of a given number. + # if n <= 1: + # return 1 + # + # result = 1 + # for num in range(2, n + 1): + # result *= num + # return result + # + # def count_permutations(sequence): + # + # # Calculates the number of unique permutations of a sequence, accounting for duplicate characters. + # permutations = factorial(len(sequence)) + # + # # Adjust for repeated characters by dividing by factorial of their counts + # for count in Counter(sequence).values(): + # permutations //= factorial(count) + # + # return permutations + + output = generate_sequences("", 0) + + return output - 1 + + +def num_tile_possibilities_with_recursion(tiles: str) -> int: + sequences: Set[str] = set() + used: List[bool] = [False] * len(tiles) + + def generate_sequences(current: str) -> None: + sequences.add(current) + + for idx, char in enumerate(tiles): + if not used[idx]: + used[idx] = True + generate_sequences(current + char) + used[idx] = False + + generate_sequences("") + return len(sequences) - 1 + + +def num_tile_possibilities_with_optimized_recursion(tiles: str) -> int: + char_count: List[int] = [0] * 26 + for letter in tiles: + char_count[ord(letter) - ord("A")] += 1 + + def generate_sequences() -> int: + result = 0 + for idx in range(26): + if char_count[idx] == 0: + continue + + result += 1 + char_count[idx] -= 1 + result += generate_sequences() + char_count[idx] += 1 + + return result + + return generate_sequences() diff --git a/algorithms/backtracking/letter_tile_possibilities/images/examples/letter_tile_possibilities_example_sample.png b/algorithms/backtracking/letter_tile_possibilities/images/examples/letter_tile_possibilities_example_sample.png new file mode 100644 index 00000000..3ca690b3 Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/examples/letter_tile_possibilities_example_sample.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_0.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_0.png new file mode 100644 index 00000000..96f8b488 Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_0.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_1.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_1.png new file mode 100644 index 00000000..a72d4379 Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_1.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_10.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_10.png new file mode 100644 index 00000000..b5588e3e Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_10.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_11.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_11.png new file mode 100644 index 00000000..5a8a5a32 Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_11.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_12.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_12.png new file mode 100644 index 00000000..eadb0bca Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_12.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_13.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_13.png new file mode 100644 index 00000000..a2cfd2dd Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_13.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_14.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_14.png new file mode 100644 index 00000000..b7204a68 Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_14.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_15.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_15.png new file mode 100644 index 00000000..58aec03b Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_15.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_16.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_16.png new file mode 100644 index 00000000..0ca83765 Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_16.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_17.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_17.png new file mode 100644 index 00000000..661c08f4 Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_17.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_18.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_18.png new file mode 100644 index 00000000..0ad1da8d Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_18.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_19.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_19.png new file mode 100644 index 00000000..41d0c83f Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_19.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_2.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_2.png new file mode 100644 index 00000000..191e133b Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_2.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_20.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_20.png new file mode 100644 index 00000000..5069646f Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_20.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_21.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_21.png new file mode 100644 index 00000000..4ce467d9 Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_21.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_3.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_3.png new file mode 100644 index 00000000..e3d8bbfa Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_3.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_4.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_4.png new file mode 100644 index 00000000..d3edae41 Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_4.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_5.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_5.png new file mode 100644 index 00000000..f9bb57bd Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_5.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_6.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_6.png new file mode 100644 index 00000000..0e8e2742 Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_6.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_7.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_7.png new file mode 100644 index 00000000..6879c7da Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_7.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_8.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_8.png new file mode 100644 index 00000000..aab6016b Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_8.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_9.png b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_9.png new file mode 100644 index 00000000..a2df4cde Binary files /dev/null and b/algorithms/backtracking/letter_tile_possibilities/images/solutions/letter_tile_possibilities_solution_9.png differ diff --git a/algorithms/backtracking/letter_tile_possibilities/test_num_tile_possibilities.py b/algorithms/backtracking/letter_tile_possibilities/test_num_tile_possibilities.py new file mode 100644 index 00000000..0845cdc3 --- /dev/null +++ b/algorithms/backtracking/letter_tile_possibilities/test_num_tile_possibilities.py @@ -0,0 +1,40 @@ +import unittest +from parameterized import parameterized +from algorithms.backtracking.letter_tile_possibilities import ( + num_tile_possibilities, + num_tile_possibilities_with_recursion, + num_tile_possibilities_with_optimized_recursion, +) + +LETTER_TILE_POSSIBILITIES = [ + ("AAB", 8), + ("ABC", 15), + ("ABC", 15), + ("CDB", 15), + ("ZZZ", 3), + ("AAABBC", 188), + ("V", 1), +] + + +class NumTilePossibilitiesTestCase(unittest.TestCase): + @parameterized.expand(LETTER_TILE_POSSIBILITIES) + def test_num_tile_possibilities(self, tiles: str, expected: int): + actual = num_tile_possibilities(tiles) + self.assertEqual(expected, actual) + + @parameterized.expand(LETTER_TILE_POSSIBILITIES) + def test_num_tile_possibilities_with_recursion(self, tiles: str, expected: int): + actual = num_tile_possibilities_with_recursion(tiles) + self.assertEqual(expected, actual) + + @parameterized.expand(LETTER_TILE_POSSIBILITIES) + def test_num_tile_possibilities_with_optimized_recursion( + self, tiles: str, expected: int + ): + actual = num_tile_possibilities_with_optimized_recursion(tiles) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/dynamic_programming/coin_change/README.md b/algorithms/dynamic_programming/coin_change/README.md index ed74a26b..f3bd84ba 100644 --- a/algorithms/dynamic_programming/coin_change/README.md +++ b/algorithms/dynamic_programming/coin_change/README.md @@ -55,7 +55,7 @@ solutions of its subproblems. To split the problem into subproblems, let's assume we know the number of coins required for some total value and the last coin denomination is C. Because of the optimal substructure property, the following equation will be true: -Min(total)=Min(total−C)+1 +`Min(total) = Min(total − C) + 1` But, we don't know what is the value of C yet, so we compute it for each element of the coins array and select the minimum from among them. This creates the following recurrence relation: @@ -84,13 +84,13 @@ three base cases to cover about what to return if the remaining amount is: - Less than zero - Equal to zero -- Neither less than zero nor equal to zero +- Neither less than zero nor equal to zero (Greater than 0) > The top-down solution, commonly known as the memoization technique, is an enhancement of the recursive solution. It > solves the problem of calculating redundant solutions over and over by storing them in an array. In the last case, when the remaining amount is neither of the base cases, we traverse the coins array, and at each -element, we recursively call the calculate_minimum_coins() function, passing the updated remaining amount remaining_amount +element, we recursively call the `calculate_minimum_coins()` function, passing the updated remaining amount remaining_amount minus the value of the current coin. This step effectively evaluates the number of coins needed for each possible denomination to make up the remaining amount. We store the return value of the base cases for each subproblem in a variable named result. We then add 1 to the result variable indicating that we're using this coin denomination in the diff --git a/algorithms/dynamic_programming/frog_jump/README.md b/algorithms/dynamic_programming/frog_jump/README.md new file mode 100644 index 00000000..d52c93f9 --- /dev/null +++ b/algorithms/dynamic_programming/frog_jump/README.md @@ -0,0 +1,84 @@ +# Frog Jump + +A frog is trying to cross a river by jumping on stones placed at various positions along the river. The river is divided +into units, and some units contain stones while others do not. The frog can only land on a stone, but it must not jump +into the water. + +You are given an array, stones, that represents the positions of the stones in ascending order (in units). The frog +starts on the first stone, and its first jump must be exactly 1 unit. + +If the frog’s last jump was of length k units, its next jump must be either k−1, k, or k+1 units. The frog can only move +forward. + +Your task is to determine whether the frog can successfully reach the last stone. + +## Constraints + +- 2 ≤ `stones.length` ≤ 1000 +- 0 ≤ `stones[i]` ≤ 2^20 − 1 +- stones[0] == 0 +- The stones array is sorted in a strictly increasing order. + +## Topics + +- Array +- Dynamic Programming + +## Solution + +The solution’s essence lies in leveraging dynamic programming to track all the jump lengths that can reach each stone. +The frog begins on the first stone with a jump length of zero, and from every reachable stone, it tries jumps of the +same size, one unit longer, or one unit shorter. Each valid move updates the DP table to record new reachable states. +The algorithm stores each stone’s position along with its index in a map, allowing for quick checks to determine if a +stone exists at a given position. Finally, if any jump length reaches the last stone, the frog can successfully cross +the river. + +Using the intuition above, we implement the algorithm as follows: +- Store the number of stones in a variable n. +- Create an unordered map, `mapper`, to store each stone’s position as the key and its index as the value for fast + lookups. +- Populate the `mapper` by mapping every stone position, `stones[i]`, to its index, `i`. +- Initialize a 2D array, `dp`, of size `1001 x 1001` with zeros to track reachable states. + > Note that this is highly dependable on the constraints, larger sizes of stones will cause an Index out of bounds + > error. So, ensure to initialize the size correctly +- Set `dp[0][0]` to `1` to indicate that the frog starts on the first stone with a previous jump of 0. +- Iterate over each stone index `i`: + - For each stone, iterate over all possible previous jump lengths `prevJump`. + - Check if `dp[i][prevJump]` is 1 to confirm that the frog can reach stone `i` with jump size `prevJump`: + - If reachable, store the current position as `currPos = stones[i]`. + - Check if a stone exists at `currPos + prevJump`; if yes, mark `dp[mapper[currPos + prevJump]][prevJump]` as `1`. + - Check if a stone exists at `currPos + prevJump + 1`; if yes, mark `dp[mapper[currPos + prevJump + 1]][prevJump + 1]` as `1` + - If `prevJump > 1` and a stone exists at `currPos + prevJump - 1`, mark `dp[mapper[currPos + prevJump - 1]][prevJump - 1]` as `1`. +- After filling the DP table, iterate over all possible jump sizes `jump` for the last stone. + - If any `dp[n - 1][jump]` equals `1`, return TRUE to indicate that the frog can cross the river. +- If no valid jump leads to the last stone, return FALSE. + +![Solution 1](./images/solutions/frog_jump_solution_1.png) +![Solution 2](./images/solutions/frog_jump_solution_2.png) +![Solution 3](./images/solutions/frog_jump_solution_3.png) +![Solution 4](./images/solutions/frog_jump_solution_4.png) +![Solution 5](./images/solutions/frog_jump_solution_5.png) +![Solution 6](./images/solutions/frog_jump_solution_6.png) +![Solution 7](./images/solutions/frog_jump_solution_7.png) +![Solution 8](./images/solutions/frog_jump_solution_8.png) +![Solution 9](./images/solutions/frog_jump_solution_9.png) +![Solution 10](./images/solutions/frog_jump_solution_10.png) +![Solution 11](./images/solutions/frog_jump_solution_11.png) +![Solution 12](./images/solutions/frog_jump_solution_12.png) +![Solution 13](./images/solutions/frog_jump_solution_13.png) +![Solution 14](./images/solutions/frog_jump_solution_14.png) +![Solution 15](./images/solutions/frog_jump_solution_15.png) +![Solution 16](./images/solutions/frog_jump_solution_16.png) +![Solution 17](./images/solutions/frog_jump_solution_17.png) +![Solution 18](./images/solutions/frog_jump_solution_18.png) +![Solution 19](./images/solutions/frog_jump_solution_19.png) +![Solution 20](./images/solutions/frog_jump_solution_20.png) + +### Time Complexity + +The algorithm’s time complexity is O(n^2), where n is the number of stones. For each stone, it may check all possible +previous jump lengths, and for each valid state, it performs constant-time lookups in the map. + +### Space Complexity + +The algorithm's space complexity is `O(n)` for the mapper map, plus `O(1)` for the fixed-size 2D DP array (1001 x 1001), assuming the constraints guarantee `n ≤ 1000`. diff --git a/algorithms/dynamic_programming/frog_jump/__init__.py b/algorithms/dynamic_programming/frog_jump/__init__.py new file mode 100644 index 00000000..5dad925f --- /dev/null +++ b/algorithms/dynamic_programming/frog_jump/__init__.py @@ -0,0 +1,38 @@ +from typing import List + + +def can_cross(stones: List[int]) -> bool: + n = len(stones) + + # Map each stone's position to its index for O(1) lookups + mapper = {stones[i]: i for i in range(n)} + + # dp[i][k] = 1 if the frog can reach stone i with a jump of length k + dp = [[0] * 2001 for _ in range(2001)] + dp[0][0] = 1 # Starting point: frog at first stone with jump size 0 + + # Try to reach every stone using all possible previous jump sizes + for i in range(n): + for prev_jump in range(n + 1): + if dp[i][prev_jump]: + curr_pos = stones[i] + + # Jump of the same length + if curr_pos + prev_jump in mapper: + dp[mapper[curr_pos + prev_jump]][prev_jump] = 1 + + # Jump one unit longer + if curr_pos + prev_jump + 1 in mapper: + dp[mapper[curr_pos + prev_jump + 1]][prev_jump + 1] = 1 + + # Jump one unit shorter (if valid) + if prev_jump > 1 and curr_pos + prev_jump - 1 in mapper: + dp[mapper[curr_pos + prev_jump - 1]][prev_jump - 1] = 1 + + # Check if the last stone is reachable with any jump size + for jump in range(n + 1): + if dp[n - 1][jump]: + return True + + # If no valid path reaches the last stone + return False diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_1.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_1.png new file mode 100644 index 00000000..555bedf5 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_1.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_10.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_10.png new file mode 100644 index 00000000..7503c6c3 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_10.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_11.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_11.png new file mode 100644 index 00000000..5ea68073 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_11.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_12.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_12.png new file mode 100644 index 00000000..502a9e5c Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_12.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_13.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_13.png new file mode 100644 index 00000000..833c42c1 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_13.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_14.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_14.png new file mode 100644 index 00000000..17fcd094 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_14.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_15.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_15.png new file mode 100644 index 00000000..7e63d0e0 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_15.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_16.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_16.png new file mode 100644 index 00000000..694224da Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_16.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_17.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_17.png new file mode 100644 index 00000000..59dd6ff4 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_17.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_18.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_18.png new file mode 100644 index 00000000..8cfcbcc6 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_18.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_19.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_19.png new file mode 100644 index 00000000..f054416a Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_19.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_2.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_2.png new file mode 100644 index 00000000..81ae5259 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_2.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_20.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_20.png new file mode 100644 index 00000000..4fe37c8a Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_20.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_3.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_3.png new file mode 100644 index 00000000..7f150505 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_3.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_4.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_4.png new file mode 100644 index 00000000..022a2982 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_4.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_5.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_5.png new file mode 100644 index 00000000..00bb2174 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_5.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_6.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_6.png new file mode 100644 index 00000000..4075bd48 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_6.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_7.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_7.png new file mode 100644 index 00000000..c96fca77 Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_7.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_8.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_8.png new file mode 100644 index 00000000..1a5e33af Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_8.png differ diff --git a/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_9.png b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_9.png new file mode 100644 index 00000000..dcb3ac9f Binary files /dev/null and b/algorithms/dynamic_programming/frog_jump/images/solutions/frog_jump_solution_9.png differ diff --git a/algorithms/dynamic_programming/frog_jump/test_frog_jump.py b/algorithms/dynamic_programming/frog_jump/test_frog_jump.py new file mode 100644 index 00000000..f81d35e6 --- /dev/null +++ b/algorithms/dynamic_programming/frog_jump/test_frog_jump.py @@ -0,0 +1,1030 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.dynamic_programming.frog_jump import can_cross + +FROG_JUMP_TEST_CASES = [ + ([0, 1], True), + ([0, 1, 3, 4, 6, 9, 13], True), + ([0, 1, 2, 4, 10, 15, 21], False), + ([0, 1, 2, 4, 7, 11], True), + ([0, 2], False), + ( + [ + 0, + 1, + 4802, + 9103, + 16575, + 22205, + 27053, + 28867, + 29267, + 30417, + 31405, + 34707, + 43336, + 53294, + 55333, + 55998, + 64793, + 68689, + 75678, + 82652, + 90431, + 95881, + 103008, + 109632, + 116318, + 116933, + 119297, + 126733, + 127952, + 132915, + 139669, + 140909, + 146981, + 151865, + 161412, + 164007, + 167313, + 168823, + 172304, + 173477, + 179161, + 183704, + 185141, + 195063, + 201550, + 204198, + 209480, + 215670, + 223347, + 226781, + 231389, + 235292, + 241493, + 246602, + 253901, + 262911, + 270903, + 272223, + 281944, + 281961, + 289697, + 298024, + 305665, + 311795, + 314918, + 320902, + 321336, + 322290, + 332101, + 339974, + 348169, + 351335, + 359734, + 367738, + 375903, + 378542, + 388182, + 396197, + 401433, + 410790, + 414915, + 415866, + 423433, + 432717, + 437566, + 442690, + 451119, + 461065, + 466730, + 472056, + 473648, + 481001, + 484792, + 494230, + 497666, + 499403, + 504660, + 512258, + 517435, + 523627, + 528086, + 536166, + 540883, + 545150, + 546571, + 546941, + 551730, + 560177, + 562279, + 566575, + 567699, + 573413, + 573836, + 582734, + 585003, + 593170, + 595909, + 597308, + 605978, + 612963, + 615716, + 621272, + 623481, + 633479, + 635586, + 636632, + 640204, + 644901, + 646532, + 651223, + 654983, + 656359, + 658108, + 664357, + 667539, + 674348, + 679495, + 680730, + 688992, + 693560, + 694063, + 701259, + 709974, + 717944, + 724252, + 727035, + 727754, + 737164, + 744124, + 752664, + 753018, + 753804, + 755128, + 759032, + 760134, + 767348, + 775216, + 779748, + 783363, + 791968, + 793968, + 796771, + 801363, + 803158, + 813155, + 821115, + 821960, + 826093, + 829883, + 835559, + 844964, + 850905, + 855267, + 863212, + 865371, + 868185, + 873230, + 882430, + 886486, + 893182, + 901773, + 910048, + 917982, + 926410, + 927337, + 937018, + 946250, + 950944, + 953586, + 955333, + 959157, + 968323, + 971715, + 975839, + 978039, + 981137, + 989012, + 992868, + 998465, + 1007758, + 1009991, + 1019392, + 1020453, + 1022286, + 1022966, + 1032741, + 1035483, + 1041434, + 1049056, + 1055151, + 1061227, + 1069400, + 1078141, + 1087379, + 1094142, + 1103011, + 1106409, + 1109258, + 1116265, + 1120875, + 1129199, + 1132299, + 1142165, + 1143688, + 1150724, + 1155998, + 1158706, + 1165480, + 1173428, + 1181991, + 1182796, + 1182998, + 1190341, + 1195549, + 1204853, + 1207888, + 1215316, + 1221906, + 1226015, + 1231983, + 1240495, + 1240595, + 1246880, + 1250381, + 1253839, + 1263274, + 1269597, + 1269942, + 1272290, + 1277126, + 1285421, + 1292280, + 1298149, + 1302027, + 1303856, + 1306006, + 1314715, + 1315733, + 1318736, + 1324358, + 1331449, + 1341377, + 1345260, + 1350688, + 1359498, + 1365529, + 1366913, + 1369025, + 1369130, + 1373711, + 1376151, + 1379711, + 1381640, + 1391328, + 1400203, + 1405356, + 1408057, + 1416142, + 1425100, + 1429025, + 1431654, + 1435895, + 1438486, + 1439232, + 1446276, + 1454672, + 1460438, + 1467815, + 1474772, + 1484209, + 1493886, + 1503812, + 1509466, + 1518045, + 1520547, + 1523127, + 1525534, + 1528415, + 1533201, + 1533988, + 1543162, + 1550720, + 1559134, + 1567005, + 1572667, + 1575703, + 1582352, + 1591815, + 1599915, + 1609505, + 1612012, + 1617538, + 1623988, + 1624198, + 1625414, + 1630419, + 1635121, + 1638608, + 1647101, + 1654194, + 1656119, + 1658305, + 1663152, + 1668475, + 1671199, + 1673428, + 1681466, + 1683648, + 1686035, + 1694826, + 1695700, + 1702239, + 1706170, + 1712727, + 1718109, + 1719227, + 1724830, + 1724839, + 1731467, + 1735854, + 1737912, + 1738037, + 1742368, + 1744123, + 1749963, + 1756155, + 1763558, + 1763915, + 1769188, + 1777210, + 1779964, + 1788397, + 1796263, + 1805194, + 1814184, + 1814921, + 1815947, + 1818677, + 1828597, + 1830373, + 1837657, + 1847019, + 1855344, + 1860941, + 1863734, + 1864533, + 1872127, + 1881060, + 1887213, + 1895335, + 1903568, + 1908411, + 1917927, + 1927459, + 1937173, + 1938316, + 1941066, + 1944617, + 1952316, + 1962163, + 1969990, + 1977991, + 1984302, + 1990786, + 1991654, + 2000906, + 2009825, + 2014816, + 2019538, + 2025009, + 2032629, + 2037412, + 2037949, + 2045057, + 2052919, + 2061207, + 2062970, + 2071619, + 2077885, + 2087818, + 2096145, + 2097187, + 2098866, + 2102856, + 2103496, + 2107017, + 2107320, + 2108528, + 2108843, + 2114108, + 2121181, + 2129615, + 2130503, + 2139371, + 2146795, + 2156596, + 2157914, + 2160418, + 2166700, + 2171657, + 2178545, + 2185566, + 2185600, + 2191593, + 2195499, + 2204904, + 2207714, + 2209075, + 2215792, + 2224919, + 2227642, + 2236875, + 2240907, + 2246831, + 2252850, + 2258464, + 2263105, + 2271426, + 2275238, + 2282886, + 2284021, + 2289254, + 2297900, + 2298316, + 2302424, + 2312099, + 2312595, + 2316125, + 2324793, + 2334290, + 2340169, + 2347810, + 2348114, + 2353076, + 2358992, + 2368312, + 2372428, + 2372782, + 2375203, + 2385088, + 2388880, + 2391227, + 2395822, + 2403946, + 2412780, + 2415637, + 2422675, + 2428972, + 2430024, + 2435845, + 2445336, + 2450767, + 2459991, + 2460081, + 2469714, + 2474044, + 2476729, + 2478842, + 2488218, + 2495846, + 2499254, + 2508837, + 2509173, + 2511503, + 2514422, + 2517611, + 2523739, + 2529492, + 2531190, + 2539865, + 2545406, + 2546484, + 2548900, + 2549493, + 2558849, + 2568792, + 2572675, + 2580663, + 2589948, + 2593029, + 2598623, + 2604108, + 2611815, + 2613976, + 2620029, + 2626127, + 2632207, + 2636399, + 2645487, + 2647904, + 2648447, + 2652197, + 2659074, + 2659573, + 2666975, + 2669422, + 2674279, + 2677538, + 2682169, + 2685602, + 2685730, + 2694845, + 2700210, + 2708955, + 2710052, + 2710238, + 2715640, + 2719782, + 2727633, + 2735158, + 2745128, + 2746348, + 2752528, + 2757317, + 2765011, + 2773041, + 2781273, + 2781455, + 2790816, + 2794608, + 2803113, + 2804947, + 2808677, + 2816979, + 2826092, + 2832070, + 2839585, + 2841082, + 2845683, + 2851716, + 2858824, + 2861955, + 2870062, + 2879917, + 2881807, + 2887952, + 2897780, + 2905577, + 2914324, + 2922210, + 2930291, + 2939438, + 2948755, + 2951069, + 2957913, + 2962275, + 2972253, + 2975364, + 2978032, + 2985508, + 2989192, + 2995785, + 2997857, + 2999284, + 3007356, + 3015575, + 3022423, + 3024608, + 3029623, + 3032486, + 3035619, + 3036741, + 3039669, + 3040333, + 3048880, + 3053923, + 3059160, + 3062778, + 3065940, + 3066569, + 3071618, + 3076161, + 3078703, + 3087682, + 3093135, + 3102572, + 3110231, + 3117585, + 3123780, + 3131290, + 3131660, + 3139448, + 3147181, + 3153097, + 3159635, + 3160786, + 3163192, + 3170455, + 3172156, + 3176047, + 3179282, + 3181548, + 3190002, + 3198110, + 3201181, + 3203734, + 3212408, + 3215228, + 3222132, + 3224284, + 3233047, + 3233712, + 3234545, + 3242908, + 3251242, + 3259546, + 3264268, + 3270276, + 3276844, + 3277964, + 3283424, + 3287438, + 3295499, + 3302101, + 3310146, + 3313635, + 3319933, + 3328349, + 3329505, + 3333245, + 3335504, + 3341466, + 3343105, + 3351704, + 3352170, + 3356292, + 3357207, + 3361677, + 3364444, + 3372406, + 3381478, + 3381579, + 3383212, + 3383829, + 3392764, + 3396988, + 3405261, + 3414652, + 3420813, + 3427897, + 3436284, + 3439024, + 3446546, + 3454872, + 3459180, + 3465367, + 3473864, + 3476916, + 3477474, + 3486861, + 3495835, + 3499814, + 3507979, + 3509821, + 3516004, + 3516702, + 3525632, + 3534535, + 3543807, + 3551844, + 3558365, + 3566524, + 3571570, + 3574770, + 3578743, + 3579730, + 3584018, + 3593487, + 3594187, + 3597336, + 3601529, + 3609651, + 3615181, + 3621820, + 3631655, + 3634790, + 3644568, + 3653246, + 3658067, + 3659049, + 3666580, + 3676150, + 3682028, + 3686679, + 3690949, + 3693928, + 3701277, + 3701475, + 3704277, + 3710832, + 3716698, + 3719986, + 3728277, + 3736305, + 3739484, + 3747938, + 3748734, + 3757573, + 3760050, + 3765011, + 3771003, + 3775882, + 3777288, + 3777783, + 3785928, + 3786478, + 3787332, + 3793172, + 3796443, + 3805842, + 3813903, + 3822928, + 3827755, + 3831261, + 3835535, + 3841776, + 3849005, + 3858189, + 3859666, + 3866984, + 3872432, + 3879859, + 3886246, + 3892508, + 3899063, + 3907914, + 3916168, + 3917771, + 3921369, + 3930025, + 3938790, + 3948607, + 3953515, + 3956870, + 3961625, + 3962233, + 3966717, + 3967834, + 3975223, + 3979802, + 3985706, + 3992329, + 3994858, + 3997325, + 3999512, + 4003714, + 4004644, + 4005120, + 4006600, + 4013009, + 4016526, + 4018170, + 4021037, + 4025110, + 4034700, + 4042903, + 4049040, + 4051134, + 4054414, + 4055422, + 4063394, + 4068427, + 4068665, + 4069219, + 4069857, + 4070418, + 4071955, + 4076529, + 4082807, + 4085488, + 4085969, + 4091596, + 4094010, + 4094425, + 4100302, + 4102717, + 4111013, + 4119665, + 4126167, + 4128612, + 4134555, + 4138163, + 4147300, + 4151708, + 4157379, + 4163125, + 4165166, + 4165831, + 4167793, + 4173195, + 4182811, + 4190236, + 4194169, + 4196363, + 4197542, + 4206889, + 4209705, + 4218824, + 4225409, + 4229387, + 4238633, + 4247786, + 4253066, + 4253375, + 4258831, + 4265851, + 4272922, + 4279712, + 4286642, + 4295689, + 4304352, + 4314304, + 4320854, + 4324424, + 4330342, + 4338565, + 4346020, + 4349885, + 4356630, + 4357558, + 4361399, + 4367249, + 4370375, + 4372822, + 4380624, + 4390070, + 4390475, + 4393532, + 4400580, + 4409685, + 4418966, + 4421674, + 4424994, + 4430831, + 4438717, + 4448318, + 4456652, + 4457848, + 4459933, + 4464970, + 4473243, + 4478683, + 4483082, + 4486974, + 4490091, + 4496417, + 4497504, + 4505157, + 4507690, + 4514842, + 4519387, + 4527995, + 4537391, + 4546511, + 4550019, + 4551395, + 4557358, + 4563242, + 4572022, + 4576469, + 4583363, + 4587225, + 4595577, + 4601357, + 4602205, + 4612192, + 4621610, + 4631570, + 4636175, + 4644247, + 4649534, + 4656468, + 4659197, + 4667721, + 4672686, + 4676773, + 4680930, + 4685557, + 4694571, + 4699079, + 4705528, + 4706405, + 4712168, + 4715817, + 4717351, + 4719192, + 4728145, + 4729997, + 4739668, + 4744370, + 4746546, + 4753962, + 4759220, + 4760485, + 4760896, + 4761321, + 4771219, + 4777845, + 4785387, + 4787381, + 4791757, + 4795554, + 4801833, + 4804651, + 4805257, + 4813421, + 4818730, + 4824410, + 4825485, + 4829939, + 4838908, + 4842625, + 4851075, + 4854026, + 4860312, + 4863754, + 4869653, + 4870150, + 4872704, + 4881927, + 4889675, + 4897966, + 4898932, + 4900251, + 4905691, + 4913960, + 4919044, + 4922054, + 4925439, + 4928109, + 4934196, + 4934710, + 4936750, + 4942337, + 4950693, + 4959068, + 4968935, + 4976953, + 4986841, + 4992879, + 4994493, + 4995134, + 5000011, + 5008638, + 5010555, + 5016729, + 5022235, + 5029805, + 5029816, + 5037171, + 5042826, + 5051956, + 5059204, + 5066349, + 5069000, + 5071014, + 5078961, + 5086464, + 5089111, + 5094790, + 5097860, + 5100594, + 5103562, + 5113186, + 5114548, + 5121427, + 5124062, + 5128064, + 5131217, + 5132549, + 5141521, + 5145911, + 5149847, + 5159119, + 5164868, + 5171645, + 5171771, + 5174628, + 5176706, + 5182156, + 5191936, + 5199093, + ], + False, + ), +] + + +class FrogJumpTestCase(unittest.TestCase): + @parameterized.expand(FROG_JUMP_TEST_CASES) + def test_can_cross(self, stones: List[int], expected: bool) -> None: + actual = can_cross(stones) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/frog_jumps/README.md b/algorithms/dynamic_programming/frog_jumps/README.md similarity index 100% rename from algorithms/frog_jumps/README.md rename to algorithms/dynamic_programming/frog_jumps/README.md diff --git a/algorithms/frog_jumps/__init__.py b/algorithms/dynamic_programming/frog_jumps/__init__.py similarity index 100% rename from algorithms/frog_jumps/__init__.py rename to algorithms/dynamic_programming/frog_jumps/__init__.py diff --git a/algorithms/frog_river_one/README.md b/algorithms/dynamic_programming/frog_river_one/README.md similarity index 100% rename from algorithms/frog_river_one/README.md rename to algorithms/dynamic_programming/frog_river_one/README.md diff --git a/algorithms/frog_river_one/__init__.py b/algorithms/dynamic_programming/frog_river_one/__init__.py similarity index 100% rename from algorithms/frog_river_one/__init__.py rename to algorithms/dynamic_programming/frog_river_one/__init__.py diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/README.md b/algorithms/dynamic_programming/max_profit_in_job_scheduling/README.md new file mode 100644 index 00000000..b3b8e519 --- /dev/null +++ b/algorithms/dynamic_programming/max_profit_in_job_scheduling/README.md @@ -0,0 +1,116 @@ +# Maximum Profit in Job Scheduling + +We have n jobs, where every job is scheduled to be done from `startTime[i]` to `endTime[i]`, obtaining a profit of `profit[i]`. + +You're given the `startTime`, `endTime` and `profit` arrays, return the maximum profit you can take such that there are no two +jobs in the subset with overlapping time range. + +If you choose a job that ends at time X you will be able to start another job that starts at time X. + +## Examples + +![Example 1](./images/examples/max_profit_in_job_scheduling_example_1.png) + +```text +Input: startTime = [1,2,3,3], endTime = [3,4,5,6], profit = [50,10,40,70] +Output: 120 +Explanation: The subset chosen is the first and fourth job. +Time range [1-3]+[3-6] , we get profit of 120 = 50 + 70. +``` + +![Example 2](./images/examples/max_profit_in_job_scheduling_example_2.png) + +```text +Input: startTime = [1,2,3,4,6], endTime = [3,5,10,6,9], profit = [20,20,100,70,60] +Output: 150 +Explanation: The subset chosen is the first, fourth and fifth job. +Profit obtained 150 = 20 + 70 + 60. +``` + +![Example 3](./images/examples/max_profit_in_job_scheduling_example_3.png) + +```text +Input: startTime = [1,1,1], endTime = [2,3,4], profit = [5,6,4] +Output: 6 +``` + +## Constraints + +- 1 <= `startTime.length` == `endTime.length` == `profit.length` <= 5 * 10^4 +- 1 <= `startTime[i]` < `endTime[i]` <= 10^9 +- 1 <= `profit[i]` <= 10^4 + +## Topics + +- Array +- Binary Search +- Dynamic Programming +- Sorting + +## Solution + +The idea behind this solution is to first sort the jobs by their end times. Afterwards, we initialize an array dp of +length len(jobs) + 1 with 0s, where dp[i] represents the maximum profit that can be earned by scheduling the first i +jobs (sorted by end time). dp[0] is initialized to 0, as scheduling 0 jobs would yield 0 profit. + +> Note: The solution below uses the key parameter in the bisect_right function to find the latest job ending before the +> start time of the current job, which is only available in Python 3.10 and above. + +![Solution 1](./images/solutions/max_profit_in_job_scheduling_solution_1.png) +![Solution 2](./images/solutions/max_profit_in_job_scheduling_solution_2.png) + +Next, we iterate over each index in dp starting from index 1. At each iteration, we calculate the maximum profit that +can be earned by scheduling the first i jobs (sorted by end time). So when i = 1, we are calculating the maximum profit +that can be earned by scheduling the first job. + +1. We find the corresponding start time and profit of the current job (jobs[i - 1]). +2. Next, we use binary search (bisect_right) to find the number of jobs that have ended before the start time of the + current job, and store this number in a variable num_jobs. dp[num_jobs] will also tell us the maximum profit that + can be earned by scheduling all the jobs that have ended before the current job. + +To breakdown our call to bisect_right: we are looking for the rightmost index in job in job with an end time +(key=lambda x: x[1]) that is less than or equal to the start time of the current job (start). + +In this case, start = 1, which means this is the first job we can possibly schedule, and num_jobs = 0. + +![Solution 3](./images/solutions/max_profit_in_job_scheduling_solution_3.png) +![Solution 4](./images/solutions/max_profit_in_job_scheduling_solution_4.png) + +Once we have this index, we have two options: + +- We can schedule the current job, in which case the profit would be `dp[num_jobs] + profit`. +- We can skip the current job, in which case the profit would be `dp[i - 1]`. + +By taking the maximum of these two options, we can calculate the maximum profit that can be earned by scheduling the +first `i` jobs. +In this case, since this is the first job we can possibly schedule, we schedule it and update dp[i] to `dp[num_jobs] + profit`. + +![Solution 5](./images/solutions/max_profit_in_job_scheduling_solution_5.png) + +The next iteration is an example of when we would choose to skip the current job. For this job, start = 3. This job +overlaps with the first job, so num_jobs = 0. If we consider the two cases: + +- We can schedule the current job, in which case the profit would be `dp[num_jobs] + profit` = 0 + 18 = 18. +- We can skip the current job, in which case the profit would be `dp[i - 1]` = 20. + +We can see that skipping the current job (in favor of choosing the previous one) would yield a higher profit, so we +update `dp[i]` to `dp[i - 1]`. + +![Solution 6](./images/solutions/max_profit_in_job_scheduling_solution_6.png) +![Solution 7](./images/solutions/max_profit_in_job_scheduling_solution_7.png) + +If we examine the state of the dp array at this point, it tells us at that the maximum profit we can obtain from +scheduling any combination of the first two jobs is 20. + +We can continue this process for the remaining jobs, and the final value of dp[-1] will tell us the maximum profit that +can be earned by scheduling all the jobs. + +### Complexity Analysis + +#### Time Complexity + +`O(n * log n)` where n is the number of jobs. This is due to the sorting of the jobs, and the binary search for each job. + +#### Space Complexity + +`O(n)` where n is the number of jobs. This is due to the dp array. diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/__init__.py b/algorithms/dynamic_programming/max_profit_in_job_scheduling/__init__.py new file mode 100644 index 00000000..4b14e726 --- /dev/null +++ b/algorithms/dynamic_programming/max_profit_in_job_scheduling/__init__.py @@ -0,0 +1,22 @@ +from typing import List +from bisect import bisect_right + + +def job_scheduling( + start_time: List[int], end_time: List[int], profit: List[int] +) -> int: + # Combine the start and end times for jobs to their profits and sort them by the end time in ascending order. + # This will incur a time complexity cost of O(n log(n)) due to sorting and space complexity of O(n) due to the use + # of the jobs array to store the jobs + jobs = sorted(zip(start_time, end_time, profit), key=lambda x: x[1]) + job_len = len(jobs) + dp = [0] * (job_len + 1) + + for i in range(1, job_len + 1): + start, end, profit = jobs[i - 1] + # find number of jobs to finish before start of current job + num_jobs = bisect_right([job[1] for job in jobs], start) + + dp[i] = max(dp[i - 1], dp[num_jobs] + profit) + + return dp[-1] diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/examples/max_profit_in_job_scheduling_example_1.png b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/examples/max_profit_in_job_scheduling_example_1.png new file mode 100644 index 00000000..bd1d2a46 Binary files /dev/null and b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/examples/max_profit_in_job_scheduling_example_1.png differ diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/examples/max_profit_in_job_scheduling_example_2.png b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/examples/max_profit_in_job_scheduling_example_2.png new file mode 100644 index 00000000..b66c308f Binary files /dev/null and b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/examples/max_profit_in_job_scheduling_example_2.png differ diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/examples/max_profit_in_job_scheduling_example_3.png b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/examples/max_profit_in_job_scheduling_example_3.png new file mode 100644 index 00000000..34349c89 Binary files /dev/null and b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/examples/max_profit_in_job_scheduling_example_3.png differ diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_1.png b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_1.png new file mode 100644 index 00000000..37df10df Binary files /dev/null and b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_1.png differ diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_2.png b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_2.png new file mode 100644 index 00000000..d6a9d54f Binary files /dev/null and b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_2.png differ diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_3.png b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_3.png new file mode 100644 index 00000000..f4dd74d4 Binary files /dev/null and b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_3.png differ diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_4.png b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_4.png new file mode 100644 index 00000000..7273f3a5 Binary files /dev/null and b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_4.png differ diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_5.png b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_5.png new file mode 100644 index 00000000..f1d30d62 Binary files /dev/null and b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_5.png differ diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_6.png b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_6.png new file mode 100644 index 00000000..398e2446 Binary files /dev/null and b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_6.png differ diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_7.png b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_7.png new file mode 100644 index 00000000..882e282b Binary files /dev/null and b/algorithms/dynamic_programming/max_profit_in_job_scheduling/images/solutions/max_profit_in_job_scheduling_solution_7.png differ diff --git a/algorithms/dynamic_programming/max_profit_in_job_scheduling/test_job_scheduling.py b/algorithms/dynamic_programming/max_profit_in_job_scheduling/test_job_scheduling.py new file mode 100644 index 00000000..b54915d8 --- /dev/null +++ b/algorithms/dynamic_programming/max_profit_in_job_scheduling/test_job_scheduling.py @@ -0,0 +1,46 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.dynamic_programming.max_profit_in_job_scheduling import job_scheduling + +JOB_SCHEDULING_TEST_CASES = [ + ( + "startTime = [1,2,3,3], endTime = [3,4,5,6], profit = [50,10,40,70]", + [1, 2, 3, 3], + [3, 4, 5, 6], + [50, 10, 40, 70], + 120, + ), + ( + "startTime = [1,2,3,4,6], endTime = [3,5,10,6,9], profit = [20,20,100,70,60]", + [1, 2, 3, 4, 6], + [3, 5, 10, 6, 9], + [20, 20, 100, 70, 60], + 150, + ), + ( + "startTime = [1,1,1], endTime = [2,3,4], profit = [5,6,4]", + [1, 1, 1], + [2, 3, 4], + [5, 6, 4], + 6, + ), +] + + +class MaxProfitInJobSchedulingTestCase(unittest.TestCase): + @parameterized.expand(JOB_SCHEDULING_TEST_CASES) + def test_job_scheduling( + self, + _, + start_time: List[int], + end_time: List[int], + profit: List[int], + expected: int, + ): + actual = job_scheduling(start_time, end_time, profit) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/dynamic_programming/maximal_rectangle/README.md b/algorithms/dynamic_programming/maximal_rectangle/README.md new file mode 100644 index 00000000..2b7ead17 --- /dev/null +++ b/algorithms/dynamic_programming/maximal_rectangle/README.md @@ -0,0 +1,103 @@ +# Maximal Rectangle + +Given a binary matrix filled with 0’s and 1’s, find the largest rectangle containing only 1’s and return its area. + +## Constraints + +- `rows` == `matrix.length` +- `cols` == `matrix[i].length` +- 1 <= `rows`, `cols` <= 200 +- `matrix[i][j]` is `0` or `1` + +## Examples + +![Example 1](./images/examples/maximal_rectangle_example_1.png) +![Example 2](./images/examples/maximal_rectangle_example_2.png) +![Example 3](./images/examples/maximal_rectangle_example_3.png) +![Example 4](./images/examples/maximal_rectangle_example_4.png) +![Example 5](./images/examples/maximal_rectangle_example_5.png) + +## Topics + +- Array +- Dynamic Programming +- Stack +- Matrix +- Monotonic Stack + +## Solution + +The problem asks us to find the largest rectangle made entirely of ‘1’s in a 2D binary matrix. Instead of checking +every possible rectangle, which would be extremely slow, we use a dynamic programming trick that turns each row into the +base of a histogram. + +As we scan each row, we keep track of how many consecutive ‘1’s appear vertically in each column. That gives us a +histogram for the current row. If a cell contains a ‘1’, its height grows; otherwise, if it’s a ‘0’, the height resets. + +Along with heights, we track how far each column can extend left and right without bumping into a ‘0’. These boundaries +effectively indicate the widest possible rectangle that can be placed on top of each histogram bar. + +Once we know the height of a bar and how far it stretches left and right, we can compute the rectangle it forms. We +repeat this for every row, taking the maximum rectangle area we encounter. The result is the largest rectangle of ‘1’s +in the entire matrix. + +The above algorithm can be broken down into the following steps. The implementation uses dynamic programming with three +arrays (height, left, right) to efficiently compute the maximal rectangle’s area: + +1. We begin by storing the dimensions of the matrix, m for rows and n for columns. +2. Next, we initialize three arrays: + - `left`: Stores the left boundary index for the rectangle at each position. + - `right`: Stores the right boundary index for the rectangle at each position. Initialized to n as an initial open + boundary. + - `height`: Stores the height of consecutive ‘1’s ending at the current row. +3. We also initialize a variable maxArea to 0 to store the compute and return the final area. +4. Then, we iterate through each row of the matrix from top to bottom: + - For each row, we initialize `current_left` and `current_right`. The counter `current_left` tracks the start of a + valid ‘1’s sequence, and `current_right` tracks the end. + - **First pass (left to right)**: We iterate through each column to update height and left boundaries. + - If `matrix[i][j]` is ‘1’ + - We increment `height[j]`, extending the column of ‘1’s. + - The new `left[j]` is the maximum of its existing value (from the row above) and `current_left`, ensuring the + boundary is valid for the current row’s ‘1’s sequence. + - Otherwise, the `matrix[i][j]` is ‘0’: + - The `height` resets to 0 as the column of ‘1’s is broken. + - `left[j]` is reset to 0, and `current_left` is updated to `j + 1`, marking the start of a new potential + sequence. + - **Second pass (right to left)**: We iterate backward to update the right boundaries and calculate the area. + - If `matrix[i][j]` is ‘1’: + - The new `right[j]` is the minimum of its existing value and `current_right`. + - Otherwise, the `matrix[i][j]` is '0': + - `right[j]` is reset to `n`, and `current_right` is updated to `j`. + - With `height`, `left`, and `right` updated for position `j`, we can calculate the area of the rectangle with + height `height[j]` and width `right[j] - left[j]`. We update maxArea with the maximum area found so far. +- After iterating through all rows, `max_area` holds the result, which we return. + +![Solution 1](./images/solutions/maximal_rectangle_solution_1.png) +![Solution 2](./images/solutions/maximal_rectangle_solution_2.png) +![Solution 3](./images/solutions/maximal_rectangle_solution_3.png) +![Solution 4](./images/solutions/maximal_rectangle_solution_4.png) +![Solution 5](./images/solutions/maximal_rectangle_solution_5.png) +![Solution 6](./images/solutions/maximal_rectangle_solution_6.png) +![Solution 7](./images/solutions/maximal_rectangle_solution_7.png) +![Solution 8](./images/solutions/maximal_rectangle_solution_8.png) +![Solution 9](./images/solutions/maximal_rectangle_solution_9.png) +![Solution 10](./images/solutions/maximal_rectangle_solution_10.png) +![Solution 11](./images/solutions/maximal_rectangle_solution_11.png) +![Solution 12](./images/solutions/maximal_rectangle_solution_12.png) +![Solution 13](./images/solutions/maximal_rectangle_solution_13.png) +![Solution 14](./images/solutions/maximal_rectangle_solution_14.png) +![Solution 15](./images/solutions/maximal_rectangle_solution_15.png) + + +### Complexity analysis + +#### Time Complexity + +O(m * n) where `m` is the number of rows and `n` is the number of columns in the input array. We iterate over each cell +once, and for each cell, we perform a constant amount of work. + +#### Space Complexity + +O(n) where `n` is the number of columns. This is because we use three auxiliary arrays (`height`, `left`, and `right`), +each of size `n`, to store the state for the current row being processed. The space required does not depend on the +number of rows `m`, only on the number of columns. diff --git a/algorithms/dynamic_programming/maximal_rectangle/__init__.py b/algorithms/dynamic_programming/maximal_rectangle/__init__.py new file mode 100644 index 00000000..61cd6a34 --- /dev/null +++ b/algorithms/dynamic_programming/maximal_rectangle/__init__.py @@ -0,0 +1,135 @@ +from typing import List + + +def maximal_rectangle(matrix: List[List[int]]) -> int: + if not matrix: + return 0 + + rows = len(matrix) + cols = len(matrix[0]) + + # These arrays track the evolving histogram across rows: + # height[j] → height of stacked '1's at column j + # left[j] → furthest left boundary where current rectangle can extend + # right[j] → furthest right boundary where current rectangle can extend + left = [0] * cols + right = [cols] * cols + height = [0] * cols + + max_area = 0 + + for i in range(rows): + current_left = 0 # dynamic left boundary for the current row + current_right = cols # dynamic right boundary for the current row + + # First forward pass: update heights and left boundaries + for j in range(cols): + if matrix[i][j] == 1: + height[j] += 1 # grow histogram bar + left[j] = max( + left[j], current_left + ) # left[j] shrinks only when we encounter a 0 + else: + height[j] = 0 # reset the bar height + left[j] = 0 # reset left boundary + current_left = j + 1 # next valid left candidate starts here + + # second backward pass: update right boundaries and compute areas + for j in range(cols - 1, -1, -1): + if matrix[i][j] == 1: + right[j] = min( + right[j], current_right + ) # right[j] shrinks whenever we hit a 0 + else: + right[j] = cols # reset boundary + current_right = j # next valid right candidate starts here + + # area of maximal rectangle using column j as part of the row floor + max_area = max(max_area, (right[j] - left[j]) * height[j]) + + return max_area + + +def maximal_rectangle_2(matrix: List[List[int]]) -> int: + """ + Returns the maximum rectangle of a grid containing only 1s in the 2D matrix + Args: + matrix(list): 2D matrix with 0s and 1s + Returns: + int: largest area of rectangle with only 1s + """ + if not matrix: + return 0 + + cols = len(matrix[0]) + heights = [0] * cols + max_area = 0 + + for row in matrix: + for col_idx, col_value in enumerate(row): + if col_value == 1: + heights[col_idx] += 1 + else: + heights[col_idx] = 0 + max_area = max(max_area, largest_rectangle_area(heights)) + + return max_area + + +def largest_rectangle_area(heights: List[int]) -> int: + """ + Find the largest rectangle area in a histogram. + + Uses monotonic stack to find the nearest smaller element on both left and right + for each bar, then calculates the maximum rectangle area. + + Args: + heights: List of bar heights in the histogram + + Returns: + Area of the largest rectangle in the histogram + """ + n = len(heights) + if n == 0: + return 0 + + # Arrays to store indices of nearest smaller elements + left_boundaries = [-1] * n # Index of nearest smaller element on the left + right_boundaries = [n] * n # Index of nearest smaller element on the right + + # Find left boundaries using monotonic increasing stack + stack = [] + for i, height in enumerate(heights): + # Pop elements >= current height + while stack and heights[stack[-1]] >= height: + stack.pop() + + # If stack not empty, top element is the nearest smaller on left + if stack: + left_boundaries[i] = stack[-1] + + stack.append(i) + + # Find right boundaries using monotonic increasing stack (traverse right to left) + stack = [] + for i in range(n - 1, -1, -1): + height = heights[i] + + # Pop elements >= current height + while stack and heights[stack[-1]] >= height: + stack.pop() + + # If stack not empty, top element is the nearest smaller on right + if stack: + right_boundaries[i] = stack[-1] + + stack.append(i) + + # Calculate maximum rectangle area + # For each bar, the rectangle extends from (left_boundary + 1) to (right_boundary - 1) + max_area = max( + height * (right_boundaries[i] - left_boundaries[i] - 1) + for i, height in enumerate(heights) + ) + + return max_area diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_1.png b/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_1.png new file mode 100644 index 00000000..9f791ff9 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_1.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_2.png b/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_2.png new file mode 100644 index 00000000..0a974ccc Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_2.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_3.png b/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_3.png new file mode 100644 index 00000000..29b51f0a Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_3.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_4.png b/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_4.png new file mode 100644 index 00000000..d1e9fb10 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_4.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_5.png b/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_5.png new file mode 100644 index 00000000..bb569d8b Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/examples/maximal_rectangle_example_5.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_1.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_1.png new file mode 100644 index 00000000..8ebe0da7 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_1.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_10.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_10.png new file mode 100644 index 00000000..ebba26c3 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_10.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_11.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_11.png new file mode 100644 index 00000000..33d0ca14 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_11.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_12.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_12.png new file mode 100644 index 00000000..30f469b3 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_12.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_13.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_13.png new file mode 100644 index 00000000..56d280e5 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_13.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_14.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_14.png new file mode 100644 index 00000000..285dc0c7 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_14.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_15.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_15.png new file mode 100644 index 00000000..e2c874bf Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_15.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_2.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_2.png new file mode 100644 index 00000000..11eeaf21 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_2.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_3.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_3.png new file mode 100644 index 00000000..00f97d72 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_3.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_4.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_4.png new file mode 100644 index 00000000..ccb3c0c6 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_4.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_5.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_5.png new file mode 100644 index 00000000..85b7e710 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_5.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_6.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_6.png new file mode 100644 index 00000000..30eff46b Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_6.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_7.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_7.png new file mode 100644 index 00000000..36139662 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_7.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_8.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_8.png new file mode 100644 index 00000000..bc3d6d0d Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_8.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_9.png b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_9.png new file mode 100644 index 00000000..2d2efd12 Binary files /dev/null and b/algorithms/dynamic_programming/maximal_rectangle/images/solutions/maximal_rectangle_solution_9.png differ diff --git a/algorithms/dynamic_programming/maximal_rectangle/test_maximal_rectangle.py b/algorithms/dynamic_programming/maximal_rectangle/test_maximal_rectangle.py new file mode 100644 index 00000000..afca2f69 --- /dev/null +++ b/algorithms/dynamic_programming/maximal_rectangle/test_maximal_rectangle.py @@ -0,0 +1,92 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.dynamic_programming.maximal_rectangle import ( + maximal_rectangle, + maximal_rectangle_2, +) + +MAXIMAL_RECTANGLE_TEST_CASES = [ + ( + [ + [1, 0, 1, 0, 1], + [1, 1, 1, 1, 1], + [0, 1, 1, 1, 0], + [1, 1, 1, 0, 1], + ], + 6, + ), + ( + [ + [1, 0, 1], + [1, 1, 1], + [1, 1, 1], + ], + 6, + ), + ( + [ + [0, 1, 1, 0], + [1, 1, 1, 1], + [1, 1, 1, 1], + [1, 1, 0, 0], + ], + 8, + ), + ( + [ + [1, 1, 0, 1], + [1, 1, 1, 1], + [1, 1, 1, 0], + ], + 6, + ), + ( + [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ], + 0, + ), + ( + [ + [1, 0, 1, 1, 1, 0, 1], + ], + 3, + ), + ( + [ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + ], + 9, + ), + ( + [ + [1, 0, 1], + [0, 1, 0], + [1, 0, 1], + ], + 1, + ), + ([[0]], 0), + ([[1, 0, 1, 0, 0], [1, 0, 1, 1, 1], [1, 1, 1, 1, 1], [1, 0, 0, 1, 0]], 6), +] + + +class MaximalRectangleTestCase(unittest.TestCase): + @parameterized.expand(MAXIMAL_RECTANGLE_TEST_CASES) + def test_maximal_rectangle(self, matrix: List[List[int]], expected: int): + actual = maximal_rectangle(matrix) + self.assertEqual(expected, actual) + + @parameterized.expand(MAXIMAL_RECTANGLE_TEST_CASES) + def test_maximal_rectangle_2(self, matrix: List[List[int]], expected: int): + actual = maximal_rectangle_2(matrix) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/dynamic_programming/word_break/__init__.py b/algorithms/dynamic_programming/word_break/__init__.py index 1d25e862..2bac82d5 100644 --- a/algorithms/dynamic_programming/word_break/__init__.py +++ b/algorithms/dynamic_programming/word_break/__init__.py @@ -83,13 +83,14 @@ def word_break_dp_tabulation(s: str, word_dict: List[str]) -> List[str]: Returns: List of valid sentences """ + word_len = len(s) # Initializing the dp table of size s.length + 1 - dp = [[] for _ in range(len(s) + 1)] + dp = [[] for _ in range(word_len + 1)] # Setting the base case dp[0] = [""] # For each substring in the input string, repeat the process. - for i in range(1, len(s) + 1): + for i in range(1, word_len + 1): prefix = s[:i] # An array to store the valid sentences formed from the current prefix being checked. @@ -109,7 +110,7 @@ def word_break_dp_tabulation(s: str, word_dict: List[str]) -> List[str]: dp[i] = temp # returning all the sentences formed from the complete string s - return dp[len(s)] + return dp[word_len] def word_break_dp_tabulation_2(s: str, word_dict: List[str]) -> List[str]: diff --git a/algorithms/dynamic_programming/word_break/test_word_break.py b/algorithms/dynamic_programming/word_break/test_word_break.py index 25894ccd..cb4a66cf 100644 --- a/algorithms/dynamic_programming/word_break/test_word_break.py +++ b/algorithms/dynamic_programming/word_break/test_word_break.py @@ -42,6 +42,9 @@ ["pine apple pen apple", "pineapple pen apple", "pine applepen apple"], ), ("catsandog", ["cats", "dog", "sand", "and", "cat"], []), + ("leetcode", ["leet", "code"], ["leet code"]), + ("applepenapple", ["apple", "pen"], ["apple pen apple"]), + ("applepenapple", ["apple", "pen"], ["apple pen apple"]), ] diff --git a/algorithms/graphs/network_delay_time/README.md b/algorithms/graphs/network_delay_time/README.md new file mode 100644 index 00000000..35701213 --- /dev/null +++ b/algorithms/graphs/network_delay_time/README.md @@ -0,0 +1,117 @@ +# Network Delay Time + +A network of n nodes labeled 1 to n is provided along with a list of travel times for directed edges represented as +times[i]=(xi, yi, ti), where xi is the source node, yi is the target node, and ti is the delay time from the source node +to the target node. + +Considering we have a starting node, k, we have to determine the minimum time required for all the remaining n−1 nodes +to receive the signal. Return −1 if it’s not possible for all n−1 nodes to receive the signal. + +## Constraints + +- 1 <= `k` <= `n` <= 100 +- 1 <= `times.length` <= 6000 +- `times[i].length` == 3 +- 1 <= x, y <= `n` +- x != y +- 0 <= t <= 100 +- Unique pairs of (x, y), which means that there should be no multiple edges + +## Examples + +![Example 1](./images/examples/network_delay_time_example_1.png) +![Example 2](./images/examples/network_delay_time_example_2.png) +![Example 3](./images/examples/network_delay_time_example_3.png) + +## Topics + +- Depth-First Search +- Breadth-First Search +- Graph Theory +- Heap (Priority Queue) +- Shortest Path + +## Solutions + +1. [Naive Approach](#naive-approach) +2. [Dijkstra's Algorithm](#optimized-approach-using-dijkstras-algorithm) + +### Naive Approach + +The naive approach is to use a simple brute force method. The algorithm would start by initializing all distances (time +to travel from source to target node) to infinity, representing disconnection between the two nodes. Then, each node +would use a nested loop to go through all other nodes and update their distances using the given travel times. If there +is no connection between a source and a target node, its distance will not be updated. After updating all distances, the +algorithm would find the maximum distance among all nodes, representing the minimum time it takes for all nodes to +receive the signal. If a node that cannot be reached from node k exists, it means the distance is infinity, and it will +return -1. + +This approach has a time complexity of `O(n^2)`, where n is the number of nodes in the graph. + +### Optimized Approach using Dijkstra's Algorithm + +Dijkstra’s algorithm is widely used for finding the shortest path between nodes in a graph. This makes it ideal for +finding the minimum delay time in a network. + +We will use an adjacency dictionary. The source node will be used as key, and the value is a list of tuples that have the +destination node and the time for the signal to travel from source to destination node. A _priority queue_ is initialized +with time initialized to 0 and starting node k as a tuple. The priority queue ensures that the node with the minimum +time is retrieved in each iteration. We will iterate over the priority queue to traverse the nodes in the network. If +the node is not visited, the time of the retrieved node is compared to the current delay time and updated accordingly. +The neighbors of the retrieved node are found using the adjacency dictionary and are added to the queue with their times +updated by adding the delay time from the retrieved node. + +Finally, if all the network nodes have been visited, we will return the computed time. Otherwise, −1 will be returned. + +> A priority queue is a queue where elements are assigned a priority and served according to their priority value. In +> our context, lower-priority elements are served before higher-priority ones. Elements with the same priority are served +> in the order they were added to the queue. + +The slides below illustrate how we would like the algorithm to run: + +![Solution 1](./images/solutions/network_delay_time_solution_1.png) +![Solution 2](./images/solutions/network_delay_time_solution_2.png) +![Solution 3](./images/solutions/network_delay_time_solution_3.png) +![Solution 4](./images/solutions/network_delay_time_solution_4.png) +![Solution 5](./images/solutions/network_delay_time_solution_5.png) +![Solution 6](./images/solutions/network_delay_time_solution_6.png) +![Solution 7](./images/solutions/network_delay_time_solution_7.png) +![Solution 8](./images/solutions/network_delay_time_solution_8.png) +![Solution 9](./images/solutions/network_delay_time_solution_9.png) +![Solution 10](./images/solutions/network_delay_time_solution_10.png) +![Solution 11](./images/solutions/network_delay_time_solution_11.png) +![Solution 12](./images/solutions/network_delay_time_solution_12.png) +![Solution 13](./images/solutions/network_delay_time_solution_13.png) +![Solution 14](./images/solutions/network_delay_time_solution_14.png) +![Solution 15](./images/solutions/network_delay_time_solution_15.png) +![Solution 16](./images/solutions/network_delay_time_solution_16.png) +![Solution 17](./images/solutions/network_delay_time_solution_17.png) +![Solution 18](./images/solutions/network_delay_time_solution_18.png) +![Solution 19](./images/solutions/network_delay_time_solution_19.png) +![Solution 20](./images/solutions/network_delay_time_solution_20.png) +![Solution 21](./images/solutions/network_delay_time_solution_21.png) +![Solution 22](./images/solutions/network_delay_time_solution_22.png) +![Solution 23](./images/solutions/network_delay_time_solution_23.png) + +#### Solution Summary + +The algorithm can be summarized in the following steps: + +1. Create an adjacency dictionary to store the information of the nodes and their edges. +2. Use a priority queue to store the nodes and their delay times. Initialize the queue with the source node and a delay + time of 0. +3. Use a visited set to track the nodes that have already been processed. +4. Process the nodes from the priority queue by first visiting the node with the smallest delay time and updating the + delay time if necessary. +5. Add the unvisited neighbors of the processed node to the priority queue with their new delay times. +6. Return the delay time if all nodes have been processed. Otherwise, return −1. + +#### Time Complexity + +This algorithm takes `O(E log(N))`, where E is the total number of edges, and N is the number of nodes in the network since +push and pop operations on the priority queue take `O(log(N))` time. + +#### Space Complexity + +The space complexity is `O(N+E)`, where N is the number of nodes in the graph, and E is the number of edges required by +the adjacency dictionary and priority queue. diff --git a/algorithms/graphs/network_delay_time/__init__.py b/algorithms/graphs/network_delay_time/__init__.py new file mode 100644 index 00000000..a010171d --- /dev/null +++ b/algorithms/graphs/network_delay_time/__init__.py @@ -0,0 +1,101 @@ +from typing import List, Dict, Tuple, Set, DefaultDict +from collections import defaultdict +from queue import PriorityQueue +import heapq + + +def network_delay_time(connections: List[List[int]], n: int, k: int) -> int: + # If there is no list of times, then we return -1 + if not connections: + return -1 + + # Graph variable will contain the vertices as key value pairs, where the key is the source vertex, the value is the + # destinations from the source vertex to other destinations. The value is a list as one can move from a vertex to + # other vertices The list is a tuple, where the first value is the destination and the second value is the weight + graph: DefaultDict[int, List[Tuple[int, int]]] = defaultdict(list) + # Vertices contain a set of all the vertices in the provided graph. This will keep track of all the vertices + # that have been provided in the 'times' argument to the function. If the length of this does not match the n argument + # then it is impossible to traverse all the nodes in the graph. + vertices: Set[int] = set() + + # iterate through the 'times' argument, populating the graph and the vertices + for time in connections: + source, destination, weight = time + graph[source].append((destination, weight)) + vertices.add(source) + vertices.add(destination) + + # If the starting vertex provided is not in the graph as a source vertex, then it is also impossible to traverse + # the entire graph from this node, so we exit early + if k not in graph: + return -1 + + # Keep track of the shortest time found so far to reach each node. Initialize each node to infinity, except node k + distances: Dict[int, int | float] = { + i: float("inf") if i != k else 0 for i in range(1, n + 1) + } + + # Minimum heap to store the smallest item at the top i.e. the vertex with the shortest time to reach, (time, node) + # We start with the initial node k, which has a time of 0, as that is the node we start with + min_heap: List[Tuple[int, int]] = [(0, k)] + heapq.heapify(min_heap) + + # Iterate through the heap + while min_heap: + # Pop off the top of the heap to get the current node with the smallest time to reach + current_time, current_node = heapq.heappop(min_heap) + # Check if the current time is greater than the distances + if current_time > distances[current_node]: + # skip this + continue + + # Discover the neighbors of the current node + neighbors = graph[current_node] + + for node, weight in neighbors: + new_total_time = current_time + weight + best_recorded_time = distances[node] + if new_total_time < best_recorded_time: + distances[node] = new_total_time + heapq.heappush(min_heap, (new_total_time, node)) + + # Keep track of the minimum time so far + minimum_time = 0 + for node, distance in distances.items(): + if distance == float("inf"): + return -1 + minimum_time = max(minimum_time, distance) + + return minimum_time + + +def network_delay_time_2(times: List[List[int]], n: int, k: int) -> int: + adjacency: DefaultDict[int, List[Tuple[int, int]]] = defaultdict(list) + for src, dst, t in times: + adjacency[src].append((dst, t)) + + pq = PriorityQueue() + pq.put((0, k)) + visited: Set[int] = set() + delays = 0 + + while not pq.empty(): + time, node = pq.get() + + if node in visited: + continue + + visited.add(node) + delays = max(delays, time) + neighbours = adjacency[node] + + for neighbour in neighbours: + neighbour_node, neighbour_time = neighbour + if neighbour_node not in visited: + new_time = time + neighbour_time + pq.put((new_time, neighbour_node)) + + if len(visited) == n: + return delays + + return -1 diff --git a/algorithms/graphs/network_delay_time/images/examples/network_delay_time_example_1.png b/algorithms/graphs/network_delay_time/images/examples/network_delay_time_example_1.png new file mode 100644 index 00000000..05810fa2 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/examples/network_delay_time_example_1.png differ diff --git a/algorithms/graphs/network_delay_time/images/examples/network_delay_time_example_2.png b/algorithms/graphs/network_delay_time/images/examples/network_delay_time_example_2.png new file mode 100644 index 00000000..f6bda38d Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/examples/network_delay_time_example_2.png differ diff --git a/algorithms/graphs/network_delay_time/images/examples/network_delay_time_example_3.png b/algorithms/graphs/network_delay_time/images/examples/network_delay_time_example_3.png new file mode 100644 index 00000000..be8e47a1 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/examples/network_delay_time_example_3.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_1.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_1.png new file mode 100644 index 00000000..3761b044 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_1.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_10.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_10.png new file mode 100644 index 00000000..e1e7e1a9 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_10.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_11.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_11.png new file mode 100644 index 00000000..6e4a26fd Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_11.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_12.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_12.png new file mode 100644 index 00000000..04d8008f Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_12.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_13.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_13.png new file mode 100644 index 00000000..29e54b76 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_13.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_14.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_14.png new file mode 100644 index 00000000..e47f82c7 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_14.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_15.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_15.png new file mode 100644 index 00000000..a65737a9 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_15.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_16.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_16.png new file mode 100644 index 00000000..7bcfca5b Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_16.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_17.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_17.png new file mode 100644 index 00000000..28b8f747 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_17.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_18.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_18.png new file mode 100644 index 00000000..2bcd7678 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_18.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_19.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_19.png new file mode 100644 index 00000000..63dede88 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_19.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_2.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_2.png new file mode 100644 index 00000000..1333627d Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_2.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_20.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_20.png new file mode 100644 index 00000000..bbda1519 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_20.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_21.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_21.png new file mode 100644 index 00000000..eb57f4d2 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_21.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_22.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_22.png new file mode 100644 index 00000000..eb397730 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_22.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_23.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_23.png new file mode 100644 index 00000000..c1798a96 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_23.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_3.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_3.png new file mode 100644 index 00000000..be402176 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_3.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_4.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_4.png new file mode 100644 index 00000000..6665c7c1 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_4.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_5.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_5.png new file mode 100644 index 00000000..93383e4e Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_5.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_6.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_6.png new file mode 100644 index 00000000..41e450a5 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_6.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_7.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_7.png new file mode 100644 index 00000000..aa12d215 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_7.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_8.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_8.png new file mode 100644 index 00000000..7e172091 Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_8.png differ diff --git a/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_9.png b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_9.png new file mode 100644 index 00000000..0e07284e Binary files /dev/null and b/algorithms/graphs/network_delay_time/images/solutions/network_delay_time_solution_9.png differ diff --git a/algorithms/graphs/network_delay_time/test_network_delay_time.py b/algorithms/graphs/network_delay_time/test_network_delay_time.py new file mode 100644 index 00000000..b158a74e --- /dev/null +++ b/algorithms/graphs/network_delay_time/test_network_delay_time.py @@ -0,0 +1,89 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.graphs.network_delay_time import ( + network_delay_time, + network_delay_time_2, +) + + +NETWORK_DELAY_TIME_TEST_CASES = [ + ( + "times=[[1,2,5],[1,3,10],[1,4,15]], n=4, k=1", + [[1, 2, 5], [1, 3, 10], [1, 4, 15]], + 4, + 1, + 15, + ), + ( + "times=[[1,2,1],[2,3,1],[3,4,1]], n=4, k=1", + [[1, 2, 1], [2, 3, 1], [3, 4, 1]], + 4, + 1, + 3, + ), + ( + "times=[[1,2,5],[2,3,10],[2,4,15]], n=4, k=1", + [[1, 2, 5], [2, 3, 10], [2, 4, 15]], + 4, + 1, + 20, + ), + ( + "times=[[1,2,5],[1,3,5],[1,4,5],[2,4,5],[3,4,5]], n=4, k=1", + [[1, 2, 5], [1, 3, 5], [1, 4, 5], [2, 4, 5], [3, 4, 5]], + 4, + 1, + 5, + ), + ( + "times=[[1,2,1],[2,3,2],[3,4,3],[4,1,4]], n=4, k=2", + [[1, 2, 1], [2, 3, 2], [3, 4, 3], [4, 1, 4]], + 4, + 2, + 9, + ), + ( + "times=[[2,1,1],[3,2,1],[3,4,2]], n=4, k=3", + [[2, 1, 1], [3, 2, 1], [3, 4, 2]], + 4, + 3, + 2, + ), + ( + "times=[[1,2,1],[2,3,1],[3,5,2]], n=5, k=1", + [[1, 2, 1], [2, 3, 1], [3, 5, 2]], + 5, + 1, + -1, + ), + ("times=[[1,2,2]], n=2, k=2", [[1, 2, 2]], 2, 2, -1), + ( + "times=[[1,2,3], [1,3,5], [2,3,1]], n=3, k=1", + [[1, 2, 3], [1, 3, 5], [2, 3, 1]], + 3, + 1, + 4, + ), + ("times=[[1,2,2], [3,2,1]], n=3, k=1", [[1, 2, 2], [3, 2, 1]], 3, 1, -1), +] + + +class NetworkDelayTimeTestCase(unittest.TestCase): + @parameterized.expand(NETWORK_DELAY_TIME_TEST_CASES) + def test_network_delay_time( + self, _, times: List[List[int]], n: int, k: int, expected: int + ): + actual = network_delay_time(times, n, k) + self.assertEqual(expected, actual) + + @parameterized.expand(NETWORK_DELAY_TIME_TEST_CASES) + def test_network_delay_time_2( + self, _, times: List[List[int]], n: int, k: int, expected: int + ): + actual = network_delay_time_2(times, n, k) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/graphs/single_cycle_check/README.md b/algorithms/graphs/single_cycle_check/README.md new file mode 100644 index 00000000..b2eed689 --- /dev/null +++ b/algorithms/graphs/single_cycle_check/README.md @@ -0,0 +1,19 @@ +# Single Cycle Check + +You are given an array of integers. Each integer represents a jump of its value in the array. For instance, the integer +2 represents a jump of 2 indices forward in the array; the integer -3 represents a jump of 3 indices backward in the +array. If a jump spills past the array's bounds, it wraps over to the other side. For instance, a jump of -1 at index 0 +brings us to the last index in the array. Similarly, a jump of 1 at the last index in the array brings us to index 0. + +Write a function that returns a boolean representing whether the jumps in the array form a single cycle. A single cycle +occurs if, starting at any index in the array and following the jumps, every element is visited exactly once before +landing back on the starting index. + +## Examples + +Example 1: + +```text +Sample input: [2, 3, 1, -4, -4, 2] +Sample output: True +``` diff --git a/algorithms/graphs/single_cycle_check/__init__.py b/algorithms/graphs/single_cycle_check/__init__.py new file mode 100644 index 00000000..035feae4 --- /dev/null +++ b/algorithms/graphs/single_cycle_check/__init__.py @@ -0,0 +1,27 @@ +from typing import List + + +def has_single_cycle(array: List[int]) -> bool: + """ + Checks if the provided array has a single cycle. + Args: + array (List[int]): list of integers. + Returns: + bool: True if the given array has a single cycle, False otherwise. + """ + n = len(array) + + def get_next_index(current_idx: int) -> int: + jump = array[current_idx] + next_index = (jump + current_idx) % n + return next_index if next_index >= 0 else next_index + n + + number_of_elements_visited = 0 + current_index = 0 + while number_of_elements_visited < n: + if number_of_elements_visited > 0 and current_index == 0: + return False + number_of_elements_visited += 1 + current_index = get_next_index(current_index) + + return current_index == 0 diff --git a/algorithms/graphs/single_cycle_check/test_single_cycle_check.py b/algorithms/graphs/single_cycle_check/test_single_cycle_check.py new file mode 100644 index 00000000..64e1ccc6 --- /dev/null +++ b/algorithms/graphs/single_cycle_check/test_single_cycle_check.py @@ -0,0 +1,19 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.graphs.single_cycle_check import has_single_cycle + +SINGLE_CYCLE_CHECK_TEST_CASES = [ + ([2, 3, 1, -4, -4, 2], True), +] + + +class SingleCycleCheckTestCase(unittest.TestCase): + @parameterized.expand(SINGLE_CYCLE_CHECK_TEST_CASES) + def test_has_single_cycle(self, array: List[int], expected: bool): + actual = has_single_cycle(array) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/graphs/valid_tree/README.md b/algorithms/graphs/valid_tree/README.md new file mode 100644 index 00000000..0096fc1a --- /dev/null +++ b/algorithms/graphs/valid_tree/README.md @@ -0,0 +1,16 @@ +# Graph Valid Tree + +Given n as the number of nodes and an array of the edges of a graph, find out if the graph is a valid tree. The nodes of +the graph are labeled from 0 to n−1, and edges[i]=[x,y] represents an undirected edge connecting the nodes x and y of +the graph. + +A graph is a valid tree when all the nodes are connected and there is no cycle between them. + +## Constraints + +- 1 <= `n` <= 1000 +- 0 <= `edges.length` <= 2000 +- `edges[i].length` == 2 +- 0 <= x, y < `n` +- x != y +- There are no repeated edges diff --git a/algorithms/graphs/valid_tree/__init__.py b/algorithms/graphs/valid_tree/__init__.py new file mode 100644 index 00000000..90521fd1 --- /dev/null +++ b/algorithms/graphs/valid_tree/__init__.py @@ -0,0 +1,30 @@ +from typing import List, DefaultDict, Set +from collections import defaultdict + + +def valid_tree(n: int, edges: List[List[int]]) -> bool: + if len(edges) > (n - 1): + return False + + graph: DefaultDict[int, List[int]] = defaultdict(list) + for edge in edges: + x, y = edge + graph[x].append(y) + graph[y].append(x) + + visited: Set[int] = set() + + def dfs(node: int, parent: int) -> bool: + if node in visited: + return False + visited.add(node) + + neighbors = graph[node] + for neighbor in neighbors: + if neighbor == parent: + continue + if not dfs(neighbor, node): + return False + return True + + return dfs(0, -1) and len(visited) == n diff --git a/algorithms/graphs/valid_tree/test_graph_valid_tree.py b/algorithms/graphs/valid_tree/test_graph_valid_tree.py new file mode 100644 index 00000000..d2bf2bbd --- /dev/null +++ b/algorithms/graphs/valid_tree/test_graph_valid_tree.py @@ -0,0 +1,33 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.graphs.valid_tree import valid_tree + +GRAPH_VALID_TREE_TEST_CASES = [ + ("n=5, edges=[[0,1],[0,2],[0,3],[3,4]]", 5, [[0, 1], [0, 2], [0, 3], [3, 4]], True), + ( + "n=5, edges=[[0,1],[0,2],[0,3],[0,4],[3,4]]", + 5, + [[0, 1], [0, 2], [0, 3], [0, 4], [3, 4]], + False, + ), + ( + "n=6, edges=[[0,1],[0,2],[1,3],[2,4],[0,5]]", + 6, + [[0, 1], [0, 2], [1, 3], [2, 4], [0, 5]], + True, + ), + ("n=4, edges=[[0,1],[0,2],[0,3]]", 4, [[0, 1], [0, 2], [0, 3]], True), + ("n=3, edges=[[0,1],[0,2],[1,2]]", 3, [[0, 1], [0, 2], [1, 2]], False), +] + + +class GraphValidTreeTestCase(unittest.TestCase): + @parameterized.expand(GRAPH_VALID_TREE_TEST_CASES) + def test_graph_valid_tree(self, _, n: int, edges: List[List[int]], expected: bool): + actual = valid_tree(n, edges) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/intervals/data_stream/README.md b/algorithms/intervals/data_stream/README.md index 2353373e..bbbe8a9d 100644 --- a/algorithms/intervals/data_stream/README.md +++ b/algorithms/intervals/data_stream/README.md @@ -104,17 +104,17 @@ Let’s look at the following illustration to get a better understanding of the Let k be the current number of disjoint intervals stored in the intervals map. -- Add Num(int value): O(logk): - - One upper bound O(logk), at most one predecessor check O(1), and up to one erase (of the next interval) plus one - insert/update (each O(logk). +- Add Num(int value): `O(log(k))`: + - One upper bound `O(log(k))`, at most one predecessor check `O(1)`, and up to one erase (of the next interval) plus + one insert/update (each `O(log(k))`). -- Get Intervals(): O(k) +- Get Intervals(): `O(k)` - We iterate over every stored interval once to build the output. -- Worst case relation to n: If there are n Add Num(int value) calls and nothing ever merges, then k=O(n), giving - Add Num(int value) O(logn) and Get Intervals() O(n). +- Worst-case relation to n: If there are n Add Num(int value) calls and nothing ever merges, then k=`O(n)`, giving + Add Num(int value) `O(log(n))` and Get Intervals() `O(n)`. ### Space Complexity -As we store only interval boundaries (start → end) rather than every number seen, the space complexity is O(k). In the -worst case with no merges, k=O(n), so space becomes O(n). +As we store only interval boundaries (start → end) rather than every number seen, the space complexity is `O(k)`. In the +worst case with no merges, k=`O(n)`, so space becomes `O(n)`. diff --git a/algorithms/sliding_window/max_sum_of_subarray/test_max_sum_sub_array.py b/algorithms/sliding_window/max_sum_of_subarray/test_max_sum_sub_array.py index ec2263c6..b39057cc 100644 --- a/algorithms/sliding_window/max_sum_of_subarray/test_max_sum_sub_array.py +++ b/algorithms/sliding_window/max_sum_of_subarray/test_max_sum_sub_array.py @@ -17,6 +17,7 @@ ([1, 2, 3, 1, 2, 3], 3, 6), ([1, 2, 1, 3, 1, 1, 1], 3, 6), ([1, 2, 3, 4, 5, 6], 5, 20), + ([2, -1, 2, 3, -2, 4, 1], 5, 8), ] diff --git a/algorithms/sliding_window/minimum_window_substring/README.md b/algorithms/sliding_window/minimum_window_substring/README.md index bb1c8d13..afb79ddf 100644 --- a/algorithms/sliding_window/minimum_window_substring/README.md +++ b/algorithms/sliding_window/minimum_window_substring/README.md @@ -1,12 +1,30 @@ # Minimum Window Substring -Given two strings s and t of lengths m and n respectively, return the minimum window -substring -of s such that every character in t (including duplicates) is included in the window. If there is no such substring, -return the empty string "". +Given two strings `s` and `t` of lengths `m` and `n` respectively, return the minimum window substring of `s` such that +every character in `t` (including duplicates) is included in the window. If there is no such substring, return the empty +string "". + +The minimum window substring in `s` should have the following properties: + +- It is the shortest substring of s that includes all of the characters present in t. +- It must contain at least the same frequency of each character as in t. +- The order of the characters does not matter here. The testcases will be generated such that the answer is unique. +> Note: If there are multiple valid minimum window substrings, return any one of them. + +## Constraints + +- Strings `s` and `t` consist of uppercase and lowercase English characters. +- 1 ≤ `s.length`, `t.length` ≤ 10^3 + +## Examples + +![Example 1](./images/examples/min_window_substring_example_1.png) +![Example 2](./images/examples/min_window_substring_example_2.png) +![Example 3](./images/examples/min_window_substring_example_3.png) + Example 1: ``` diff --git a/algorithms/sliding_window/minimum_window_substring/__init__.py b/algorithms/sliding_window/minimum_window_substring/__init__.py index 8919feff..09afd651 100644 --- a/algorithms/sliding_window/minimum_window_substring/__init__.py +++ b/algorithms/sliding_window/minimum_window_substring/__init__.py @@ -1,7 +1,64 @@ +from typing import List, Tuple, Dict from collections import Counter def min_window(s: str, t: str) -> str: + # If `t` is empty, return an empty string as no window is possible + if not t: + return "" + + # Dictionaries to store the required character counts and the current window's character counts + req_count = {} + window = {} + + # Populate `req_count` with the character frequencies of `t` + for char in t: + req_count[char] = req_count.get(char, 0) + 1 + + # Variables to track the number of characters that match the required frequencies + current = ( + 0 # Count of characters in the current window that meet the required frequency + ) + required = len(req_count) # Total number of unique characters in `t` + + # Result variables to track the best window + res = [-1, -1] # Stores the start and end indices of the minimum window + res_len = float("inf") # Length of the minimum window + + # Sliding window pointers + left = 0 # Left pointer of the window + for right in range(len(s)): + char = s[right] + + # If `char` is in `t`, update the window count + if char in req_count: + window[char] = window.get(char, 0) + 1 + # If the frequency of `char` in the window matches the required frequency, update `current` + if window[char] == req_count[char]: + current += 1 + + # Try to contract the window while all required characters are present + while current == required: + # Update the result if the current window is smaller than the previous best + if (right - left + 1) < res_len: + res = [left, right] + res_len = right - left + 1 + + # Shrink the window from the left + left_char = s[left] + if left_char in req_count: + # Decrement the count of `left_char` in the window + window[left_char] -= 1 + # If the frequency of `left_char` in the window is less than required, update `current` + if window[left_char] < req_count[left_char]: + current -= 1 + left += 1 # Move the left pointer to shrink the window + + # Return the minimum window if found, otherwise return an empty string + return s[res[0] : res[1] + 1] if res_len != float("inf") else "" + + +def min_window_2(s: str, t: str) -> str: if len(t) > len(s): return "" if t == s: @@ -16,14 +73,14 @@ def min_window(s: str, t: str) -> str: # Filter all the characters from s into a new list along with their index. # The filtering criteria is that the character should be present in t. - filtered_s = [] + filtered_s: List[Tuple[int, str]] = [] for idx, char in enumerate(s): if char in counter_t: filtered_s.append((idx, char)) left, right = 0, 0 formed = 0 - window_counts = {} + window_counts: Dict[str, int] = {} ans = float("inf"), None, None # Look for the characters only in the filtered list instead of entire s. This helps to reduce our search. diff --git a/algorithms/sliding_window/minimum_window_substring/images/examples/min_window_substring_example_1.png b/algorithms/sliding_window/minimum_window_substring/images/examples/min_window_substring_example_1.png new file mode 100644 index 00000000..32604e6e Binary files /dev/null and b/algorithms/sliding_window/minimum_window_substring/images/examples/min_window_substring_example_1.png differ diff --git a/algorithms/sliding_window/minimum_window_substring/images/examples/min_window_substring_example_2.png b/algorithms/sliding_window/minimum_window_substring/images/examples/min_window_substring_example_2.png new file mode 100644 index 00000000..999d1f52 Binary files /dev/null and b/algorithms/sliding_window/minimum_window_substring/images/examples/min_window_substring_example_2.png differ diff --git a/algorithms/sliding_window/minimum_window_substring/images/examples/min_window_substring_example_3.png b/algorithms/sliding_window/minimum_window_substring/images/examples/min_window_substring_example_3.png new file mode 100644 index 00000000..5c720f0a Binary files /dev/null and b/algorithms/sliding_window/minimum_window_substring/images/examples/min_window_substring_example_3.png differ diff --git a/algorithms/sliding_window/minimum_window_substring/test_min_window_substring.py b/algorithms/sliding_window/minimum_window_substring/test_min_window_substring.py new file mode 100644 index 00000000..247c1f43 --- /dev/null +++ b/algorithms/sliding_window/minimum_window_substring/test_min_window_substring.py @@ -0,0 +1,30 @@ +import unittest +from parameterized import parameterized +from algorithms.sliding_window.minimum_window_substring import min_window, min_window_2 + +MIN_WINDOW_SUBSTRING_TEST_CASES = [ + ("ADOBECODEBANC", "ABC", "BANC"), + ("a", "a", "a"), + ("a", "aa", ""), + ("ABCD", "ABC", "ABC"), + ("XYZYX", "XYZ", "XYZ"), + ("ABXYZJKLSNFC", "ABC", "ABXYZJKLSNFC"), + ("AAAAAAAAAAA", "A", "A"), + ("ABDFGDCKAB", "ABCD", "DCKAB"), +] + + +class MinWindowSubstringTestCase(unittest.TestCase): + @parameterized.expand(MIN_WINDOW_SUBSTRING_TEST_CASES) + def test_min_window_substring(self, s: str, t: str, expected: str): + actual = min_window(s, t) + self.assertEqual(expected, actual) + + @parameterized.expand(MIN_WINDOW_SUBSTRING_TEST_CASES) + def test_min_window_substring_2(self, s: str, t: str, expected: str): + actual = min_window_2(s, t) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/sliding_window/substring_concatenation/README.md b/algorithms/sliding_window/substring_concatenation/README.md new file mode 100644 index 00000000..fa432adc --- /dev/null +++ b/algorithms/sliding_window/substring_concatenation/README.md @@ -0,0 +1,131 @@ +# Substring with Concatenation of All Words + +You are given a string, s, and an array of strings, words. All strings in words are of the same length. + +A concatenated string is a string that contains all the words in words exactly once, in any order, concatenated together +without any intervening characters. + +Formally, a concatenated string is a permutation of all words joined together. For example, if words = ["ab", "cd", "ef"], +then the following are all valid concatenated strings: "abcdef", "abefcd", "cdabef", "cdefab", "efabcd", "efcdab". +However, "acdbef" is not valid because it is not formed by concatenating all the words in any order. + +Your task is to return all starting indices of substrings in s that are concatenated strings. + +You may return the indices in any order. + +## Constraints + +- 1 ≤ `s.length` ≤ 10^3 +- 1 ≤ `words.length` ≤ 1000 +- 1 ≤ `words[i].length` ≤ 30 +- `s` and `words[i]` consist of lowercase English letters. + +## Examples + +![Example 1](./images/examples/substring_with_concatenation_of_all_words_example_1.png) +![Example 2](./images/examples/substring_with_concatenation_of_all_words_example_2.png) +![Example 3](./images/examples/substring_with_concatenation_of_all_words_example_3.png) +![Example 4](./images/examples/substring_with_concatenation_of_all_words_example_4.png) +![Example 5](./images/examples/substring_with_concatenation_of_all_words_example_5.png) +![Example 6](./images/examples/substring_with_concatenation_of_all_words_example_6.png) + +Example 1: + +```text +Input: s = "barfoothefoobarman", words = ["foo","bar"] + +Output: [0,9] + +Explanation: + +The substring starting at 0 is "barfoo". It is the concatenation of ["bar","foo"] which is a permutation of words. +The substring starting at 9 is "foobar". It is the concatenation of ["foo","bar"] which is a permutation of words. +``` + +Example 2: + +```text +Input: s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"] + +Output: [] + +Explanation: + +There is no concatenated substring. +``` + +Example 3: + +```text +Input: s = "barfoofoobarthefoobarman", words = ["bar","foo","the"] + +Output: [6,9,12] + +Explanation: + +The substring starting at 6 is "foobarthe". It is the concatenation of ["foo","bar","the"]. +The substring starting at 9 is "barthefoo". It is the concatenation of ["bar","the","foo"]. +The substring starting at 12 is "thefoobar". It is the concatenation of ["the","foo","bar"]. +``` + +## Topics + +- Hash Table +- String +- Sliding Window + +## Solution + +The essence of this problem lies in recognizing that all words share the same length. This uniformity allows the +algorithm to traverse the string in consistent, word-sized steps, ensuring that every examined segment corresponds +exactly to a full word rather than arbitrary substrings. Instead of checking every possible substring combination, the +algorithm uses this structure to efficiently locate positions where a sequence of words forms a valid concatenation. The +key idea is to treat the text as a grid of equally sized word blocks and slide through it using a fixed stride. + +To achieve this efficiently, the algorithm applies a sliding window strategy. The window moves across the string, +maintaining two frequency maps: +1. Expected word frequencies: How many times should each word appear in a valid combination? +2. Current word frequencies: How many times does each word actually appear in the current window of text? + +As the window slides through the string: +- If a word appears more often than allowed, the algorithm moves the left boundary forward to discard the extra + occurrences. +- If a word doesn’t belong to the given set at all, the window resets immediately. +- Whenever the current window contains exactly all required words with matching counts, the starting index of that + window is recorded as a valid result. + +By dynamically adjusting the window’s boundaries and avoiding redundant checks, this approach achieves near-linear +performance relative to the string’s length. +The steps of the algorithm are as follows: + +1. Initialize the following variables: + - `word_len`: The length of one word + - `num_words`: The total number of words + - `total_len`: Total length of all concatenated words `(word_len * num_words)` + - `word_count`: A frequency map built from the `words` list +2. Iterate over starting offsets in the range `[0, word_len - 1]`. This ensures alignment with word boundaries and + considers every possible window. Start a sliding window aligned to that offset. +3. For each offset, initialize two pointers, `left` and `right`, and an empty counter seen. While `right + word_len <= len(s)`: + - Extract the current word: `word = s[right:right + word_len]`. + - Move `right` forward in steps of `word_len`. + - If the word exists in `word_count`: + - Increment its count in `seen`. + - If its frequency exceeds what’s allowed in `word_count`, move `left` forward (shrinking the window) until counts + are valid again. + - If `word` is not in `word_count`: + - Clear the `seen` counter. + - Move `left` to `right` to reset the window. + - Check for valid concatenation: + - If the current window size equals total_len, record the starting index left in the result list as a valid starting + index. +4. After processing all offsets, return the `result` list of all valid starting indices. + +### Time Complexity + +The time complexity of the above solution is O(n×m), where n equals the length of s and m equals the length of each word, +because each character is visited in sliding windows across `word_len` offsets. + +### Space Complexity + +The space complexity of the above solution is O(k), where k equals the number of unique words, for storing frequency +counts in the Counter objects (word_count and seen). diff --git a/algorithms/sliding_window/substring_concatenation/__init__.py b/algorithms/sliding_window/substring_concatenation/__init__.py new file mode 100644 index 00000000..525b3742 --- /dev/null +++ b/algorithms/sliding_window/substring_concatenation/__init__.py @@ -0,0 +1,94 @@ +from typing import List +from collections import Counter + + +def find_substring(s: str, words: List[str]) -> List[int]: + if not s or not words: + return [] + + word_len = len(words[0]) + word_count = len(words) + total_len = word_len * word_count + word_freq = Counter(words) + result = [] + + # Check each possible word alignment offset (0, 1, ..., word_len - 1) + for i in range(word_len): + left = i + curr_freq = Counter() + count = 0 + + # Slide the window across the string in steps of word_len + for j in range(i, len(s) - word_len + 1, word_len): + word = s[j : j + word_len] + + if word in word_freq: + curr_freq[word] += 1 + count += 1 + + # If we have too many of this word, shrink from the left + while curr_freq[word] > word_freq[word]: + left_word = s[left : left + word_len] + curr_freq[left_word] -= 1 + count -= 1 + left += word_len + + # Success! The window contains all words exactly once + if count == word_count: + result.append(left) + else: + # Invalid word: reset the current window and move 'left' past 'j' + curr_freq.clear() + count = 0 + left = j + word_len + + return result + + +def find_substring_2(s: str, words: List[str]) -> List[int]: + if not s or not words: + return [] + + # Each word has the same length + word_len = len(words[0]) + # Total number of words + num_words = len(words) + # Total length of a valid substring (all words concatenated) + total_len = word_len * num_words + # Frequency count of all words in the list + word_count = Counter(words) + # List to store all valid starting indices + result = [] + + # Check all possible starting offsets within word length range + for i in range(word_len): + left = i # Left pointer for sliding window + right = i # Right pointer for sliding window + seen = Counter() # Tracks words seen in the current window + + # Slide the window across the string + while right + word_len <= len(s): + # Extract a word-sized substring + word = s[right : right + word_len] + right += word_len + + # If the word exists in the target list + if word in word_count: + seen[word] += 1 # Count it in the current window + + # If we’ve seen a word too many times, shrink the window from the left + while seen[word] > word_count[word]: + left_word = s[left : left + word_len] + seen[left_word] -= 1 + left += word_len + + # If the total window matches the length of all words, record index + if right - left == total_len: + result.append(left) + else: + # Invalid word — reset the window + seen.clear() + left = right + + # Return all valid starting indices + return result diff --git a/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_1.png b/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_1.png new file mode 100644 index 00000000..31a973a4 Binary files /dev/null and b/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_1.png differ diff --git a/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_2.png b/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_2.png new file mode 100644 index 00000000..d0d7fcba Binary files /dev/null and b/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_2.png differ diff --git a/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_3.png b/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_3.png new file mode 100644 index 00000000..45243ba3 Binary files /dev/null and b/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_3.png differ diff --git a/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_4.png b/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_4.png new file mode 100644 index 00000000..96bac0b1 Binary files /dev/null and b/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_4.png differ diff --git a/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_5.png b/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_5.png new file mode 100644 index 00000000..2bc99865 Binary files /dev/null and b/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_5.png differ diff --git a/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_6.png b/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_6.png new file mode 100644 index 00000000..451fce71 Binary files /dev/null and b/algorithms/sliding_window/substring_concatenation/images/examples/substring_with_concatenation_of_all_words_example_6.png differ diff --git a/algorithms/sliding_window/substring_concatenation/test_substring_with_concatenation_of_words.py b/algorithms/sliding_window/substring_concatenation/test_substring_with_concatenation_of_words.py new file mode 100644 index 00000000..3481c6d2 --- /dev/null +++ b/algorithms/sliding_window/substring_concatenation/test_substring_with_concatenation_of_words.py @@ -0,0 +1,42 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.sliding_window.substring_concatenation import ( + find_substring, + find_substring_2, +) + +SUBSTRING_WITH_CONCATENATION_OF_WORDS_TEST_CASES = [ + ("barfoothefoobarman", ["foo", "bar"], [0, 9]), + ("wordgoodgoodgoodbestword", ["word", "good", "best", "word"], []), + ("barfoofoobarthefoobarman", ["bar", "foo", "the"], [6, 9, 12]), + ("catcatdogdog", ["cat", "dog"], [3]), + ( + "lingmindraboofooowingdingbarrwingmonkeypoundcake", + ["fooo", "barr", "wing", "ding", "wing"], + [13], + ), + ("foobarfoo", ["foo"], [0, 6]), + ("oneonetwo", ["one", "two"], [3]), + ("abcdabcabcabcd", ["abc", "abc"], [4, 7]), +] + + +class SubstringWithConcatenationOfWordsTestCase(unittest.TestCase): + @parameterized.expand(SUBSTRING_WITH_CONCATENATION_OF_WORDS_TEST_CASES) + def test_substring_with_concatenation_of_word( + self, s: str, words: List[str], expected: List[int] + ): + actual = find_substring(s, words) + self.assertEqual(expected, actual) + + @parameterized.expand(SUBSTRING_WITH_CONCATENATION_OF_WORDS_TEST_CASES) + def test_substring_with_concatenation_of_word_2( + self, s: str, words: List[str], expected: List[int] + ): + actual = find_substring_2(s, words) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/two_pointers/sort_by_parity/README.md b/algorithms/two_pointers/sort_by_parity/README.md new file mode 100644 index 00000000..8c911b3a --- /dev/null +++ b/algorithms/two_pointers/sort_by_parity/README.md @@ -0,0 +1,90 @@ +# Sort Array By Parity II + +You are given an integer array, nums, where exactly half of the elements are even, and the other half are odd. + +Rearrange nums such that: + +- All even numbers are placed at even indexes [0,2,4,...] +- All odd numbers are placed at odd indexes [1,3,5,...] + +You may return any valid arrangement that satisfies these conditions. + +## Constraints: + +- 2 ≤ `nums.length` ≤ 10^3 +- `nums.length` is even. +- Half of the integers in `nums` are even. +- 0 ≤ `nums[i]` ≤ 1000 + +## Examples + +Example 1 + +```text +Input: nums = [4,2,5,7] +Output: [4,5,2,7] +Explanation: [4,7,2,5], [2,5,4,7], [2,7,4,5] would also have been accepted. +``` + +Example 2 + +```text +Input: nums = [2,3] +Output: [2,3] +``` + +## Topics + +- Array +- Two Pointers +- Sorting + +## Solution + +The essence of this solution is to use a modified cyclic sort approach combined with the flavor of two pointers to +rearrange the array such that even numbers are placed at even indexes and odd numbers at odd indexes. The approach uses +two pointers: an even pointer for even indexes and an odd pointer for odd indexes. The even pointer starts at index 0 +and moves across even indexes, while the odd pointer starts at index 1 and moves across odd indexes. The algorithm +iterates through the array and checks whether the numbers at these indices are correctly placed. If the number at the +even pointer is even, the pointer moves forward to the next even index. Similarly, if the number at the odd pointer is +odd, it moves forward to the next odd index. However, if the number at the even pointer is odd and the number at the +odd pointer is even, they are misplaced and need to be swapped. After swapping, both pointers continue to their respective +next positions. This process repeats until all elements are correctly placed. The algorithm restores order with minimal +operations by ensuring that misplaced numbers are swapped into their correct positions. Although inspired by the principles +of cyclic sort, where elements are placed in their correct positions, this approach uses a modified cyclic sort approach +that organizes elements based on parity rather than numerical value. It is an in-place approach, making it both simple +and optimal. + +Now, let’s look at the steps of the solution: + +1. We initialize two pointers, i = 0 and j = 1. i moves through even indexes [0,2,4,...] and j moves through odd indexes + [1,3,5,...]. +2. We iterate through the array until both i and j are within the bounds of nums. + - If nums[i] is even (correct placement), we move i forward by 2 to check the next even index because it is already + at its correct index. + - If nums[j] is odd (correct placement), we move j forward by 2 to check the next odd index because it is already at + its correct index. + - Otherwise, if nums[i] is odd and nums[j] is even, we swap them to fix their positions. This follows a modified + cyclic sort approach where elements are swapped directly to their rightful places without repeated scanning. After + swapping, both pointers (i and j) are moved forward by 2. +3. After iterating through the array, we return the modified nums, where all even numbers appear at even indexes, and + all odd numbers at odd indexes. + +![Solution 1](./images/solutions/sort_by_parity_2_solution_1.png) +![Solution 2](./images/solutions/sort_by_parity_2_solution_2.png) +![Solution 3](./images/solutions/sort_by_parity_2_solution_3.png) +![Solution 4](./images/solutions/sort_by_parity_2_solution_4.png) +![Solution 5](./images/solutions/sort_by_parity_2_solution_5.png) +![Solution 6](./images/solutions/sort_by_parity_2_solution_6.png) +![Solution 7](./images/solutions/sort_by_parity_2_solution_7.png) +![Solution 8](./images/solutions/sort_by_parity_2_solution_8.png) +![Solution 9](./images/solutions/sort_by_parity_2_solution_9.png) +![Solution 10](./images/solutions/sort_by_parity_2_solution_10.png) + +### Time Complexity + +The time complexity of the solution is O(n) because each element in nums is processed at most once. + +### Space Complexity + +Sorting is done in place without extra memory usage, so the space complexity of the solution is O(1) diff --git a/algorithms/two_pointers/sort_by_parity/__init__.py b/algorithms/two_pointers/sort_by_parity/__init__.py new file mode 100644 index 00000000..eecae8a0 --- /dev/null +++ b/algorithms/two_pointers/sort_by_parity/__init__.py @@ -0,0 +1,30 @@ +from typing import List + + +def sort_array_by_parity_2(nums: List[int]) -> List[int]: + if not nums: + return [] + # Initialize two pointers: + # even_idx moves across even indexes [0, 2, 4,..] + # odd_idx moves across odd indexes [1, 3, 5,..] + even_idx = 0 + odd_idx = 1 + + # Traverse the array while both pointers are within bounds + while even_idx < len(nums) and odd_idx < len(nums): + # If number at even index 'i' is even, it's correctly placed + if nums[even_idx] % 2 == 0: + # Move to the next even index + even_idx += 2 + # If number at odd index 'j' is odd, it's correctly placed + elif nums[odd_idx] % 2 == 1: + # Move to the next odd index + odd_idx += 2 + else: + # If misplaced (odd at even index or even at odd index), swap them + nums[even_idx], nums[odd_idx] = nums[odd_idx], nums[even_idx] + # Move both pointers forward after swapping + even_idx += 2 + odd_idx += 2 + + return nums diff --git a/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_1.png b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_1.png new file mode 100644 index 00000000..bd675cf0 Binary files /dev/null and b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_1.png differ diff --git a/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_10.png b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_10.png new file mode 100644 index 00000000..a50259fe Binary files /dev/null and b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_10.png differ diff --git a/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_2.png b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_2.png new file mode 100644 index 00000000..d641fd4e Binary files /dev/null and b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_2.png differ diff --git a/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_3.png b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_3.png new file mode 100644 index 00000000..ce3b1b5d Binary files /dev/null and b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_3.png differ diff --git a/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_4.png b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_4.png new file mode 100644 index 00000000..27c72ae2 Binary files /dev/null and b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_4.png differ diff --git a/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_5.png b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_5.png new file mode 100644 index 00000000..c617e0ca Binary files /dev/null and b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_5.png differ diff --git a/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_6.png b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_6.png new file mode 100644 index 00000000..b4026adf Binary files /dev/null and b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_6.png differ diff --git a/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_7.png b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_7.png new file mode 100644 index 00000000..1baa32d2 Binary files /dev/null and b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_7.png differ diff --git a/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_8.png b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_8.png new file mode 100644 index 00000000..9ab18649 Binary files /dev/null and b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_8.png differ diff --git a/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_9.png b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_9.png new file mode 100644 index 00000000..607a4fba Binary files /dev/null and b/algorithms/two_pointers/sort_by_parity/images/solutions/sort_by_parity_2_solution_9.png differ diff --git a/algorithms/two_pointers/sort_by_parity/test_sort_by_parity.py b/algorithms/two_pointers/sort_by_parity/test_sort_by_parity.py new file mode 100644 index 00000000..860a7ba2 --- /dev/null +++ b/algorithms/two_pointers/sort_by_parity/test_sort_by_parity.py @@ -0,0 +1,26 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.two_pointers.sort_by_parity import sort_array_by_parity_2 + +SORT_BY_PARITY_II_TEST_CASES = [ + ([3, 6, 1, 4], [6, 3, 4, 1]), + ([2, 21, 12, 1], [2, 21, 12, 1]), + ([0, 0, 1, 1], [0, 1, 0, 1]), + ([100, 200, 300, 400], [100, 200, 300, 400]), + ([10, 100, 1000, 10000], [10, 100, 1000, 10000]), + ([4, 2, 5, 7], [4, 5, 2, 7]), + ([2, 3], [2, 3]), + ([3, 0, 4, 0, 2, 1, 3, 1, 3, 4], [0, 3, 4, 3, 2, 1, 0, 1, 4, 3]), +] + + +class SortArrayByParityIITestCase(unittest.TestCase): + @parameterized.expand(SORT_BY_PARITY_II_TEST_CASES) + def test_sort_array_by_parity(self, nums: List[int], expected: List[int]): + actual = sort_array_by_parity_2(nums) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/datastructures/trees/binary/search_tree/README.md b/datastructures/trees/binary/search_tree/README.md index 75173fcf..26488a69 100755 --- a/datastructures/trees/binary/search_tree/README.md +++ b/datastructures/trees/binary/search_tree/README.md @@ -362,3 +362,118 @@ balanced binary search tree, it will be O(logn). ### Space Complexity The space complexity of the solution is O(1) because we don’t use any additional space. + +--- + +## Is Valid Binary Search Tree + +Given the root of a binary tree, determine if it is a valid binary search tree (BST). + +A tree is a BST if the following conditions are met: + +Every node on the left subtree has a value less than the value of the current node. +Every node on the right subtree has a value greater than the value of the current node. +The left and right subtrees must also be valid BSTs. + +### Examples + +![Example Valid BST 1](./images/examples/is_valid_bst_example_1.png) + +Input: [2,1,4] +Output: True + +![Example Valid BST 2](./images/examples/is_valid_bst_example_2.png) + +Input: [4,1,5,null,null,3,6] +Output: False. 3 is the the root node's right subtree, but it is less than the root node 4. + +### Solution + +Let's think about what it means for a binary tree to be a valid binary search tree. For a binary tree to be a valid +binary search tree, the following conditions must be true: + +- Every node in the left subtree of the root node must have a value less than the value of the root node. +- Every node in the right subtree of the root node must have a value greater than the value of the root node. + +This definition is true for every subtree in the node. + +We can use that definition to validate a binary search tree by having parents pass values down to their children. Let's +say we are validating this binary search tree: + +![Solution Valid BST 1](./images/solutions/is_valid_bst_solution_1.png) + +Based on the definition of a valid binary search tree, any value in the left subtree must be less than 4, but there is +no limit to how small the values are. + +So we can pass max = 4 and min = -inf to the left child as a range of valid values of the left subtree. + +![Solution Valid BST 2](./images/solutions/is_valid_bst_solution_2.png) + +From there, for the subtree rooted at node 2: +The max value of any node in the left subtree 2 is 2, and the min value is still -inf. + +![Solution Valid BST 3](./images/solutions/is_valid_bst_solution_3.png) + +Any value in the right subtree must be greater than the 2, but also less than 4 (the value of the root node). So we can +pass max = 4 and min = 2 to the right child as a range of valid values of the right subtree. + +![Solution Valid BST 4](./images/solutions/is_valid_bst_solution_4.png) + +We can visit each node in the tree, and have the parent pass down the range of valid values to their children in this +fashion. If the current node's value falls outside of the valid range, we can return False immediately. If we reach the +empty subtree, this means that we have not yet found an invalid node yet, and we can return True. + +#### Return Values + +If I'm at a node in the tree, what values do I need from my left and right children to tell if the current subtree is a +valid binary search tree? + +The current subtree is a valid binary search tree if: +- The left subtree is a valid binary search tree. +- The right subtree is a valid binary search tree. +- And the value of the current node falls within the valid range. + +This tells me that each recursive call should return a boolean value indicating whether the current subtree is a valid +binary search tree. + +#### Base Case + +An empty tree is a valid binary search tree. + +#### Extra Work + +The work that we need to do at each node is to check if the current node's value falls within the valid range. If it +doesn't we can return False immediately. + +#### Helper Functions + +Since we need to pass the minimum and maximum values down to their children, we need to introduce a helper function to +keep track of these values. + +This helper function will introduce two parameters, min_ and max_, which represent the range of values that the current +subtree's nodes can take on. The helper function will return a boolean value indicating whether the current subtree is +a valid binary search tree. + +When we recurse to our left child, we: +- Pass the current node's value as the new max_ value, since the left child's value must be less than the current node's + value. min_ remains the same. + +When we recurse to our right child, we: +- Pass the current node's value as the new min_ value, since the right child's value must be greater than the current + node's value. max_ remains the same. + +#### Global Variables + +The return value of the helper function matches the answer to the problem, so we don't need to use any global variables. + +#### Complexity Analysis + +##### Time Complexity + +O(N) where N is the number of nodes in the binary tree. We visit each node via each recursive call to dfs exactly once. +Each recursive call does a constant amount of work. + +##### Space Complexity + +O(N) where N is the number of nodes in the binary tree, for the space that it takes to allocate each recursive call +frame on the call stack. diff --git a/datastructures/trees/binary/search_tree/__init__.py b/datastructures/trees/binary/search_tree/__init__.py index 64714baf..c8342547 100755 --- a/datastructures/trees/binary/search_tree/__init__.py +++ b/datastructures/trees/binary/search_tree/__init__.py @@ -1,867 +1,6 @@ -from typing import List, Any, Optional -from datastructures.trees.binary.node import BinaryTreeNode +from datastructures.trees.binary.search_tree.binary_search_tree import BinarySearchTree +from datastructures.trees.binary.search_tree.bst_iterator import ( + BinarySearchTreeIterator, +) -from datastructures.queues.fifo import FifoQueue -from datastructures.stacks.dynamic import DynamicSizeStack -from datastructures.trees import T -from datastructures.trees.binary.tree import BinaryTree - - -class BinarySearchTree(BinaryTree): - def __init__(self, root: Optional[BinaryTreeNode] = None): - super().__init__(root) - self.stack = DynamicSizeStack() - - def __len__(self) -> int: - if not self.root: - return 0 - - counter = 1 - stack = DynamicSizeStack() - stack.push(self.root) - - while not stack.is_empty(): - node = stack.pop() - - if node.left: - counter += 1 - stack.push(node.left) - - if node.right: - counter += 1 - stack.push(node.right) - - return counter - - @staticmethod - def construct_bst(items: List[T]) -> Optional["BinarySearchTree"]: - """ - Constructs a binary search tree from a sorted list of items. - - This method works by recursively finding the middle element of the list and assigning it to the root of the tree. - The left and right subtrees are then constructed by recursively calling the method on the left and right halves - of the list. - - @param items: A sorted list of items to construct the tree from. - @return: A binary search tree constructed from the items. - """ - if not items: - return None - - def construct_bst_helper(left: int, right: int) -> Optional[BinaryTreeNode]: - # base case for the method if the left is greater than the right - if left > right: - return None - - # Find the middle index - this element becomes the root - # Using (left + right) // 2 ensures we get the left-middle for even-length arrays - mid = (left + right) // 2 - root = BinaryTreeNode(items[mid]) - root.left = construct_bst_helper(left, mid - 1) - root.right = construct_bst_helper(mid + 1, right) - return root - - return BinarySearchTree(root=construct_bst_helper(0, len(items) - 1)) - - def insert_node(self, data: Optional[T]): - """ - Inserts a node in a BST given an element - If there is no root, then create a new root node with the data and return it - - If there is a root, then check the data against the root node's data and determine if it should go left or right - If the data is greater than the root node's data, then go right, if the data is less than the root node's data, - then go left. Repeat this operation, until we can insert the node in the right place. - """ - if not self.root: - self.root = BinaryTreeNode(data) - return - - def insert_helper(value: T, node: BinaryTreeNode) -> BinaryTreeNode: - if not node: - return BinaryTreeNode(data) - if value < node.data: - node.left = insert_helper(value, node.left) - else: - node.right = insert_helper(value, node.right) - return node - - if data: - insert_helper(data, self.root) - - def delete_node(self, key: T) -> Optional[BinaryTreeNode]: - """Deletes a node from the Binary Search Tree. If the node is found, it is deleted and the tree re-ordered to - remain a valid binary search tree. - - This will be typically an O(log(n) operation because deletion requires a search plus a few extra steps to deal - with any hanging children. - - Args: - key (T): Value to delete from binary search tree - - Return: - BinaryTreeNode: New root of binary search tree - """ - # nothing to delete here as root is none - if self.root is None: - return self.root - - def delete_helper( - value: T, node: Optional[BinaryTreeNode] - ) -> Optional[BinaryTreeNode]: - # base case when we have hit the bottom of the tree, and the parent node has no children - if node is None: - return None - # if the value to delete is less than or greater than the current node, we set the left or right child - # respectively to be the return value of a recursive call of this very method on the current node's left or - # right subtree - elif value < node.data: - node.left = delete_helper(value, node.left) - - # we return the current node (and its subtree if existent) to be used as the new value of its parent's - # left or right child - return node - elif value > node.data: - node.right = delete_helper(value, node.right) - return node - # if the current node is the one we want to delete - elif value == node.data: - # if the current node has no left child, we delete it by returning it's right child (and it's subtree if - # existent) to be its parent's new subtree - if node.left is None: - return node.right - # if the node has no left nor right child, this ends up being None as per the first line of code in this - # function - elif node.right is None: - return node.left - # if the current node has 2 children, we delete the current node by calling the lift function, which - # changes the current node's value to the value of it's successor node - else: - node.right = lift(node.right, node) - return node - return None - - def lift( - node: BinaryTreeNode, node_to_delete: BinaryTreeNode - ) -> BinaryTreeNode: - # if the current node of this function has a left child, we recursively call this function to continue down - # the left subtree to find the successor node - if node.left is not None: - node.left = lift(node.left, node_to_delete) - return node - # if the current node has no left child, that means the current node of this function is the successor node - # and we take its value and make it the new value of the node that we are deleting. - else: - node_to_delete.data = node.data - # we return the successor node's right child to be now used as its parent's left child - return node.right - - return delete_helper(key, self.root) - - @property - def height(self) -> int: - if self.root is None: - return 0 - - # if we don't have either left and right nodes from the root, we return 1 - if not self.root.left and not self.root.right: - return 1 - - height = 0 - queue = FifoQueue() - queue.enqueue(self.root) - - while True: - current_level_nodes = queue.size - - if current_level_nodes == 0: - return height - - height += 1 - - while current_level_nodes > 0: - node = queue.dequeue() - - if node.left is not None: - queue.enqueue(node.left) - - if node.right is not None: - queue.enqueue(node.right) - current_level_nodes -= 1 - - def next(self) -> int: - """ - Returns the next smallest number in a BST. - This involves two major operations. One is where we pop an element from the stack which becomes the next smallest element to return. This is a O(1) operation. - However, we then make a call to our helper function _leftmost_inorder which iterates over a bunch of nodes. This is clearly a linear time operation i.e. O(N) in the worst case. - However, the important thing to note here is that we only make such a call for nodes which have a right child. Otherwise, we simply return. - Also, even if we end up calling the helper function, it won't always process N nodes. They will be much lesser. Only if we have a skewed tree would there be N nodes for the root. - But that is the only node for which we would call the helper function. - - Thus, the amortized (average) time complexity for this function would still be O(1). We don't need to have a solution which gives constant time operations for every call. - We need that complexity on average and that is what we get. - """ - - # this is the smallest element in the BST - topmost_node = self.stack.pop() - - # if the node has a right child, call the helper function for the right child to - # get the next smallest item - # We don't need to check for the left child because of the way we have added nodes onto the stack. - # The topmost node either won't have a left child or would already have the left subtree processed. - # If it has a right child, then we call our helper function on the node's right child. - # This would comparatively be a costly operation depending upon the structure of the tree - if topmost_node.right: - self.__leftmost_inorder(topmost_node.right) - - return topmost_node.data - - def has_next(self) -> bool: - """ - Checks if the BST has items left. Since this uses a stack, then we simply check if the stack still has items. - This is used in an iterator approach to getting items from the BST. This returns True if there are items & False - otherwise, the Time Complexity here is O(1) - """ - return self.stack.is_empty() - - def __leftmost_inorder(self, root: BinaryTreeNode) -> None: - # Add all the nodes of the left most branch to the stack - while root: - self.stack.push(root) - root = root.left - - def find_largest(self, node: BinaryTreeNode) -> BinaryTreeNode: - """ - Simply finds the largest node in a BST. We walk rightward down the BST until the current node has no right child - and return it. This assumes that the Tree is a valid BST - :param node: root node, or current node - :return: Value of the largest node - :rtype: object - """ - current = node - while current: - if not current.right: - return current - current = current.right - - return current - - def find_second_largest(self) -> BinaryTreeNode: - """ - Finds the second largest node in the Binary Search Tree given a root node - :return: Value of the second largest Node - :rtype: object - """ - if not self.root or (not self.root.left and not self.root.right): - raise Exception("Tree must have at least 2 nodes") - - current = self.root - - while current: - # case: current is largest and has a left subtree - # 2nd largest is the largest in that subtree - if current.left and not current.right: - return self.find_largest(current.left) - - # case: current is parent of largest, and - # largest has no children, so - # current is 2nd largest - if current.right and not current.right.left and not current.right.right: - return current - - current = current.right - - return current - - def find_kth_largest(self, k: int) -> Optional[BinaryTreeNode]: - """ - Finds the kth largest value in a binary search tree. This uses a reverse in order traversal moving from right - to root to left until the kth largest node can be found. We don't have to traverse the whole tree since binary - search trees are already ordered following the property of the right subtree has nodes which have the left - sub-tree always less than their parent and the right subtree has nodes with values that are either equal to or - greater than the parent. With this property in mind, we perform a reverse in order traversal to be able to move - from right to root to left to find the kth largest node in the tree. - - Complexity: - Time: O(h + k): where h is the height of the tree and k is the input - Space: O(h): where h is the height of the tree. - - Args: - k (int): The kth largest value to find - Returns: - Optional[BinaryTreeNode]: The kth largest node - """ - - # This is a helper class that helps to track the algorithm's progress of traversing the tree - class TreeInfo: - def __init__(self): - self.number_of_nodes_visited: int = 0 - self.latest_visited_node: Optional[BinaryTreeNode] = None - - def reverse_in_order_traverse(node: BinaryTreeNode, tree_information: TreeInfo): - """ - Helper function to traverse the tree in reverse in order - Args: - node (BinaryTreeNode): The node to traverse - tree_information (TreeInfo): The tree information - """ - # base case: if node is None or we've already found the kth largest - if not node or tree_information.number_of_nodes_visited >= k: - return - - # traverse right subtree first for larger values - reverse_in_order_traverse(node.right, tree_information) - - # Visit the current node if we haven't found k nodes yet - if tree_information.number_of_nodes_visited < k: - tree_information.number_of_nodes_visited += 1 - tree_information.latest_visited_node = node - - # traverse the left subtree for smaller values if needed - reverse_in_order_traverse(node.left, tree_information) - - tree_info = TreeInfo() - reverse_in_order_traverse(self.root, tree_info) - return tree_info.latest_visited_node - - def find_closest_value_in_bst(self, target: T) -> Optional[BinaryTreeNode]: - """ - Finds the closest value in the binary search tree to the given target value. - - Args: - target T: Value to search for - Returns: - Node with the closest value to the target - """ - # edge case for empty nodes, if none is provided, we can't find a value that is close to the target - if not self.root: - return None - - # if the node's data is the target, exit early by returning it - if self.root.data == target: - return self.root - - # this keeps track of the minimum on both the left and the right - closest_node = self.root - min_diff = abs(target - self.root.data) - current = self.root - - # while the queue is not empty, we pop off nodes from the queue and check for their values - while current: - current_diff = abs(target - self.root.data) - - if current_diff < min_diff: - min_diff = current_diff - closest_node = current - - if current.data == target: - return current - - if target < current.data: - current = current.left - else: - current = current.right - - return closest_node - - def range_sum(self, low: int, high: int): - """ - returns the sum of datas of all nodes with a data in the range [low, high]. - - Example: - Input: root = [10,5,15,3,7,null,18], low = 7, high = 15 - Output: 32 - - Uses an iterative approach to solving the problem with Breadth First Search Traversal - - Complexity Analysis: - - Time Complexity: O(N), where N is the number of nodes in the tree - - Space Complexity: O(N) - - The stack will contain no more than 2 levels of the nodes. The maximal number of nodes in a binary tree is - N/2. Therefore, the maximal space needed for the stack is O(N) - """ - - ans = 0 - - stack = [self.root] - - while stack: - node = stack.pop() - - if node: - if low <= node.data <= high: - ans += node.data - - if low < node.data: - if node.left: - stack.append(node.left) - - if node.data < high: - if node.right: - stack.append(node.right) - - return ans - - def breadth_first_search(self) -> List[Any]: - """ - Performs a breadth first search through a Binary Tree - This will traverse the tree level by level and depth by depth. Using a Queue to put elements into the queue - """ - queue = FifoQueue() - - # start off by adding the root node - queue.enqueue(self.root) - - # while the queue is not empty, we want to traverse the tree and add elements to the queue, - while not queue.is_empty(): - current_node = queue.dequeue() - - if current_node.left: - queue.enqueue(current_node.left) - - if current_node.right: - queue.enqueue(current_node.right) - - def pre_order(self) -> List[Any]: - """ - Type of Depth First Traversal (DFS) for binary trees which will start at root node and proceed to the left - data and print it until it reaches the leaf(node with no more children) and then backtrack to the node and - check if the current node has a right child and print it. This will continue until all nodes have been - tracked and printed. - """ - result = [] - stack = DynamicSizeStack() - - if not self.root: - return result - - current = self.root - - while current or not stack.is_empty(): - while current: - result.append(current.data) - stack.push(current) - current = current.left - - current = stack.pop() - - current = current.right - - return result - - def increasing_order_traversal(self) -> Optional[BinaryTreeNode]: - if not self.root: - return None - - def inorder(node: BinaryTreeNode): - if node: - yield from inorder(node.left) - yield node.data - yield from inorder(node.right) - - result = current = BinaryTreeNode(None) - for data in inorder(self.root): - current.right = BinaryTreeNode(data) - current = current.right - - return result.right - - def in_order_recurse(self, node: BinaryTreeNode) -> List[T]: - """ - Another type of Depth First Search (DFS) that traverses the tree from the left to middle to right of the tree. - This type of search will begin at the left node and check if that node has a left child and continually check - until that left node is a leaf(has no children) and will then print its data and "bubble up" back to the - current node and execute that (in this case print it) and then print the right node. The same procedure is - executed for the right side of the tree. - """ - result = [] - if self.root: - if self.root.left: - self.in_order_recurse(self.root.left) - - result.append(self.root.data) - - if self.root.right: - self.in_order_recurse(self.root.right) - return result - - def in_order_iterate(self) -> List[T]: - """ - Iterative approach using a stack - """ - result = [] - stack = DynamicSizeStack() - current = self.root - - while current or not stack.is_empty(): - while current: - stack.push(current) - current = current.left - - current = stack.pop() - result.append(current.data) - current = current.right - - return result - - def in_order_morris_traversal(self) -> List[Any]: - result = [] - current = self.root - pre = None - - while current: - if not current.left: - # add the current data of the node - result.append(current.data) - # Move to next right node - current = current.right - else: - # we have a left subtree - pre = current.left - - # find rightmost - while pre.right: - pre = pre.right - - # put current after the pre node - pre.right = current - # store current node - temp = current - # move current to top of new tree - current = current.left - # original current left be None, avoid infinite loops - temp.left = None - - return result - - def post_order(self) -> List[Any]: - """ - 1. Push root to first stack. - 2. Loop while first stack is not empty - 2.1 Pop a node from first stack and push it to second stack - 2.2 Push left and right children of the popped node to first stack - 3. Print contents of second stack - """ - if not self.root: - return [] - - # create 2 stacks - stack_one = DynamicSizeStack() - stack_two = DynamicSizeStack() - data = [] - - # push root to stack one - stack_one.push(self.root) - - # while stack 1 is not empty - while stack_one: - # pop a node from stack 1 and add it to stack 2 - node = stack_one.pop() - stack_two.push(node) - - # push left & right children of removed item to stack one - if node.left: - stack_one.push(node.left) - - if node.right: - stack_one.push(node.right) - - while stack_two: - node = stack_two.pop() - data.append(node.data) - - return data - - def get_depth(self) -> int: - pass - - def level_order_traversal(self) -> List[Any]: - pass - - def pre_order_traversal(self) -> List[Any]: - data = [] - if not self.root: - return data - - def pre_order_helper(root: BinaryTreeNode): - if not root: - return - data.append(root.data) - pre_order_helper(root.left) - pre_order_helper(root.right) - - pre_order_helper(self.root) - return data - - def is_binary_search_tree(self): - """ - Checks if a binary search tree is valid. A data of None is a valid Binary Search Tree - Complexity Analysis: - Space Complexity: O(n) as we are using a stack to keep track of the nodes, where n is the height of the tree - Time Complexity: O(n), worst case we travers all the nodes of the tree(left sub tree and right sub tree) to - check if the tree is a valid BST - :rtype: bool - :return: Boolean True if valid, False otherwise - """ - - # Tree with no root is still valid - if not self.root: - return True - - # start with the root with an arbitrarily low lower bound and an arbitrarily higher bound - stack = [(float("-inf"), self.root, float("inf"))] - - # depth first traversal - while stack: - lower_bound, node, upper_bound = stack.pop() - - if not node: - continue - - # if this node is invalid, return false immediately - if node.data <= lower_bound or node.data >= upper_bound: - return False - - if node.left: - # this node must be less than the current node - stack.append((lower_bound, node.left, node.data)) - - if node.right: - # this node must be greater than the current node - stack.append((node.data, node.right, upper_bound)) - - # if none of the nodes are invalid, return true - # at this point we have checked all the nodes - return True - - def is_binary_search_tree_recursive( - self, root: BinaryTreeNode, lower_bound=-float("inf"), upper_bound=float("inf") - ): - """ - This uses the call stack to check if the binary search tree node is valid. - This will work, but is vulnerable to stack overflow error - Possible :exception: OverflowError - :param root: Binary search tree node to check for - :param lower_bound: the lower bound set arbitrarily - :param upper_bound: upper bound set arbitrarily - :return: True/False if the root is a valid binary search tree - :rtype: bool - """ - - if not root: - return True - - # if the data is out of bounds - if root.data > upper_bound or root.data < lower_bound: - return False - - return not ( - not self.is_binary_search_tree_recursive(root.left, lower_bound, root.data) - or not self.is_binary_search_tree_recursive( - root.right, root.data, upper_bound - ) - ) - - def search_node(self, data: T) -> bool: - """ - Searches for the given data in a binary search tree. If the data exists in the tree, then True is returned, - else false - Arguments: - data: data to search for - """ - - def search_helper(current: Optional[BinaryTreeNode], value: T) -> bool: - """ - Search helper that is used to search the binary search tree for the given value - Arguments: - current: the current binary search tree node, could be a missing value - value: the value to search for - """ - if current: - if current.data == value: - return True - elif current.data < value: - return search_helper(current.right, value) - else: - return search_helper(current.left, value) - return False - - return search_helper(self.root, data) - - def merge_trees(self, other_node: BinaryTreeNode) -> BinaryTreeNode: - """ - Merges this tree with another tree given another node - :param other_node Other Root node, may be None, therefore we return the root node if availables - :type BinaryTreeNode - :returns Binary Tree Node - """ - if not other_node: - return self.root - - if not self.root: - return other_node - - self.root.data += other_node.data - - self.root.left = self.merge_trees(other_node.left) - self.root.right = self.merge_trees(other_node.right) - - return self.root - - def is_balanced(self) -> bool: - """ - Checks if a binary tree is balanced - :return: True/False, if a binary tree is balanced - :rtype: bool - """ - if self.root is None: - return True - - tree_root = self.root - - # short circuit as soon as we find more than 2 - depths = [] - - # treat this list as a stack, that will store tuples of (node, depth) - # give the stack a maximum size of the length of the tree root - # alternatively, we could use a list - # nodes = [] - nodes = DynamicSizeStack() - - # nodes.append((tree_root, 0)) - nodes.push((tree_root, 0)) - - while len(nodes): - # pop a node and its depth from the top of a stack - node, depth = nodes.pop() - - # case, we found a leaf - if not node.left and not node.right: - # we only care if it is a new depth - if depth not in depths: - depths.append(depth) - - # two ways we might now have an unbalanced tree: - # 1) more than 2 different leaf depths - # 2) 2 leaf depths that are more than 1 apart - if len(depths) > 2 or ( - len(depths) == 2 and abs(depths[0] - depths[1]) > 1 - ): - return False - - # case, this is not a leaf, keep stepping down - else: - if node.left: - nodes.push((node.left, depth + 1)) - if node.right: - nodes.push((node.right, depth + 1)) - - return True - - def lowest_common_ancestor( - self, node_one: BinaryTreeNode, node_two: BinaryTreeNode - ) -> BinaryTreeNode: - """ - Considering it is a BST, we can assume that this tree is a valid BST, we could also check for this - If both of the datas in the 2 nodes provided are greater than the root node, then we move to the right. - if the nodes are less than the root node, we move to the left. - If there is no root node, then we exit and return None, as no common ancestor could exist in such a case with - no root node. - - Assumptions: - - assumes that the node itself can also be an ancestor/descendant of itself - - Complexity Analysis: - - Time Complexity: O(h). - The Time Complexity of the above solution is O(h), where h is the height of the tree. - - Space Complexity: O(1). - The space complexity of the above solution is constant. - """ - - if not self.root: - return None - - # if any of the node datas matches the data data for the root node, return the root node - if self.root.data == node_one.data or self.root.data == node_two.data: - return self.root - - while self.root: - # if both node_one and node_two are smaller than root, then LCA lies in the left - if self.root.data > node_one.data and self.root.data > node_two.data: - self.root = self.root.left - - # if both node_one and node_two are greater than root, then LCA lies in the right - elif self.root.data < node_one.data and self.root.data < node_two.data: - self.root = self.root.right - else: - break - - return self.root - - @property - def paths(self) -> list: - """ - Gets all the paths of this tree from root node to leaf nodes - """ - if not self.root: - return [] - - stack = DynamicSizeStack() - stack.push((self.root, "")) - res = [] - - while stack: - node, path = stack.pop() - - if not (node.left or node.right): - res.append(path + str(node.data)) - - if node.left: - stack.push((node.left, path + str(node.data) + "->")) - - if node.right: - stack.push((node.right, path + str(node.data) + "->")) - - return [list(map(int, x.split("->"))) for x in res] - - def inorder_successor(self, node: BinaryTreeNode) -> Optional[BinaryTreeNode]: - """ - Returns the inorder successor of the node. If there is no node, None is returned. The inorder successor of the - node is the node with the smallest value greater than node.data in the binary search tree. - - This assumes that the node is in the tree already. - - Complexity: - - Time Complexity: The time complexity of this solution is O(n) in the worst-case scenario where the given tree - is skewed. However, for a balanced binary search tree, it will be O(logn). - - Space Complexity: O(1) because we don't use any additional space. - - Args: - node (BinaryTreeNode): node to search for inorder successor - Returns: - Optional[BinaryTreeNode]: returns inorder successor of node if available, else None - """ - if not self.root: - return None - - successor = None - current = self.root - - # current is the best candidate so far - while current: - # when current.data is greater than the node.data, we have found a valid successor, so, we save it in the - # successor variable - if current.data > node.data: - successor = current - # Move left to find a better(smaller) candidate. By moving to current.left, we are exploring values that - # are smaller than current.data. Since we want the smallest possible successor, we must check the left - # side of current to see if there is a node that is still greater than node.data, but close to node.data - current = current.left - else: - # this node is too small, so we must go right to find a larger value - current = current.right - - return successor +__all__ = ["BinarySearchTree", "BinarySearchTreeIterator"] diff --git a/datastructures/trees/binary/search_tree/binary_search_tree.py b/datastructures/trees/binary/search_tree/binary_search_tree.py new file mode 100755 index 00000000..c814dfb7 --- /dev/null +++ b/datastructures/trees/binary/search_tree/binary_search_tree.py @@ -0,0 +1,907 @@ +from typing import List, Any, Optional +from datastructures.trees.binary.node import BinaryTreeNode + +from datastructures.queues.fifo import FifoQueue +from datastructures.stacks.dynamic import DynamicSizeStack +from datastructures.trees import T +from datastructures.trees.binary.tree import BinaryTree + + +class BinarySearchTree(BinaryTree): + def __init__(self, root: Optional[BinaryTreeNode] = None): + super().__init__(root) + self.stack = DynamicSizeStack() + + def __len__(self) -> int: + if not self.root: + return 0 + + counter = 1 + stack = DynamicSizeStack() + stack.push(self.root) + + while not stack.is_empty(): + node = stack.pop() + + if node.left: + counter += 1 + stack.push(node.left) + + if node.right: + counter += 1 + stack.push(node.right) + + return counter + + @staticmethod + def construct_bst(items: List[T]) -> Optional["BinarySearchTree"]: + """ + Constructs a binary search tree from a sorted list of items. + + This method works by recursively finding the middle element of the list and assigning it to the root of the tree. + The left and right subtrees are then constructed by recursively calling the method on the left and right halves + of the list. + + @param items: A sorted list of items to construct the tree from. + @return: A binary search tree constructed from the items. + """ + if not items: + return None + + def construct_bst_helper(left: int, right: int) -> Optional[BinaryTreeNode]: + # base case for the method if the left is greater than the right + if left > right: + return None + + # Find the middle index - this element becomes the root + # Using (left + right) // 2 ensures we get the left-middle for even-length arrays + mid = (left + right) // 2 + root = BinaryTreeNode(items[mid]) + root.left = construct_bst_helper(left, mid - 1) + root.right = construct_bst_helper(mid + 1, right) + return root + + return BinarySearchTree(root=construct_bst_helper(0, len(items) - 1)) + + def insert_node(self, data: Optional[T]): + """ + Inserts a node in a BST given an element + If there is no root, then create a new root node with the data and return it + + If there is a root, then check the data against the root node's data and determine if it should go left or right + If the data is greater than the root node's data, then go right, if the data is less than the root node's data, + then go left. Repeat this operation, until we can insert the node in the right place. + """ + if not self.root: + self.root = BinaryTreeNode(data) + return + + def insert_helper(value: T, node: BinaryTreeNode) -> BinaryTreeNode: + if not node: + return BinaryTreeNode(data) + if value < node.data: + node.left = insert_helper(value, node.left) + else: + node.right = insert_helper(value, node.right) + return node + + if data: + insert_helper(data, self.root) + + def delete_node(self, key: T) -> Optional[BinaryTreeNode]: + """Deletes a node from the Binary Search Tree. If the node is found, it is deleted and the tree re-ordered to + remain a valid binary search tree. + + This will be typically an O(log(n) operation because deletion requires a search plus a few extra steps to deal + with any hanging children. + + Args: + key (T): Value to delete from binary search tree + + Return: + BinaryTreeNode: New root of binary search tree + """ + # nothing to delete here as root is none + if self.root is None: + return self.root + + def delete_helper( + value: T, node: Optional[BinaryTreeNode] + ) -> Optional[BinaryTreeNode]: + # base case when we have hit the bottom of the tree, and the parent node has no children + if node is None: + return None + # if the value to delete is less than or greater than the current node, we set the left or right child + # respectively to be the return value of a recursive call of this very method on the current node's left or + # right subtree + elif value < node.data: + node.left = delete_helper(value, node.left) + + # we return the current node (and its subtree if existent) to be used as the new value of its parent's + # left or right child + return node + elif value > node.data: + node.right = delete_helper(value, node.right) + return node + # if the current node is the one we want to delete + elif value == node.data: + # if the current node has no left child, we delete it by returning it's right child (and it's subtree if + # existent) to be its parent's new subtree + if node.left is None: + return node.right + # if the node has no left nor right child, this ends up being None as per the first line of code in this + # function + elif node.right is None: + return node.left + # if the current node has 2 children, we delete the current node by calling the lift function, which + # changes the current node's value to the value of it's successor node + else: + node.right = lift(node.right, node) + return node + return None + + def lift( + node: BinaryTreeNode, node_to_delete: BinaryTreeNode + ) -> BinaryTreeNode: + # if the current node of this function has a left child, we recursively call this function to continue down + # the left subtree to find the successor node + if node.left is not None: + node.left = lift(node.left, node_to_delete) + return node + # if the current node has no left child, that means the current node of this function is the successor node + # and we take its value and make it the new value of the node that we are deleting. + else: + node_to_delete.data = node.data + # we return the successor node's right child to be now used as its parent's left child + return node.right + + return delete_helper(key, self.root) + + @property + def height(self) -> int: + if self.root is None: + return 0 + + # if we don't have either left and right nodes from the root, we return 1 + if not self.root.left and not self.root.right: + return 1 + + height = 0 + queue = FifoQueue() + queue.enqueue(self.root) + + while True: + current_level_nodes = queue.size + + if current_level_nodes == 0: + return height + + height += 1 + + while current_level_nodes > 0: + node = queue.dequeue() + + if node.left is not None: + queue.enqueue(node.left) + + if node.right is not None: + queue.enqueue(node.right) + current_level_nodes -= 1 + + def next(self) -> int: + """ + Returns the next smallest number in a BST. + This involves two major operations. One is where we pop an element from the stack which becomes the next smallest element to return. This is a O(1) operation. + However, we then make a call to our helper function _leftmost_inorder which iterates over a bunch of nodes. This is clearly a linear time operation i.e. O(N) in the worst case. + However, the important thing to note here is that we only make such a call for nodes which have a right child. Otherwise, we simply return. + Also, even if we end up calling the helper function, it won't always process N nodes. They will be much lesser. Only if we have a skewed tree would there be N nodes for the root. + But that is the only node for which we would call the helper function. + + Thus, the amortized (average) time complexity for this function would still be O(1). We don't need to have a solution which gives constant time operations for every call. + We need that complexity on average and that is what we get. + """ + + # this is the smallest element in the BST + topmost_node = self.stack.pop() + + # if the node has a right child, call the helper function for the right child to + # get the next smallest item + # We don't need to check for the left child because of the way we have added nodes onto the stack. + # The topmost node either won't have a left child or would already have the left subtree processed. + # If it has a right child, then we call our helper function on the node's right child. + # This would comparatively be a costly operation depending upon the structure of the tree + if topmost_node.right: + self.__leftmost_inorder(topmost_node.right) + + return topmost_node.data + + def has_next(self) -> bool: + """ + Checks if the BST has items left. Since this uses a stack, then we simply check if the stack still has items. + This is used in an iterator approach to getting items from the BST. This returns True if there are items & False + otherwise, the Time Complexity here is O(1) + """ + return self.stack.is_empty() + + def __leftmost_inorder(self, root: BinaryTreeNode) -> None: + # Add all the nodes of the left most branch to the stack + while root: + self.stack.push(root) + root = root.left + + def find_largest(self, node: BinaryTreeNode) -> BinaryTreeNode: + """ + Simply finds the largest node in a BST. We walk rightward down the BST until the current node has no right child + and return it. This assumes that the Tree is a valid BST + :param node: root node, or current node + :return: Value of the largest node + :rtype: object + """ + current = node + while current: + if not current.right: + return current + current = current.right + + return current + + def find_second_largest(self) -> BinaryTreeNode: + """ + Finds the second largest node in the Binary Search Tree given a root node + :return: Value of the second largest Node + :rtype: object + """ + if not self.root or (not self.root.left and not self.root.right): + raise Exception("Tree must have at least 2 nodes") + + current = self.root + + while current: + # case: current is largest and has a left subtree + # 2nd largest is the largest in that subtree + if current.left and not current.right: + return self.find_largest(current.left) + + # case: current is parent of largest, and + # largest has no children, so + # current is 2nd largest + if current.right and not current.right.left and not current.right.right: + return current + + current = current.right + + return current + + def find_kth_largest(self, k: int) -> Optional[BinaryTreeNode]: + """ + Finds the kth largest value in a binary search tree. This uses a reverse in order traversal moving from right + to root to left until the kth largest node can be found. We don't have to traverse the whole tree since binary + search trees are already ordered following the property of the right subtree has nodes which have the left + sub-tree always less than their parent and the right subtree has nodes with values that are either equal to or + greater than the parent. With this property in mind, we perform a reverse in order traversal to be able to move + from right to root to left to find the kth largest node in the tree. + + Complexity: + Time: O(h + k): where h is the height of the tree and k is the input + Space: O(h): where h is the height of the tree. + + Args: + k (int): The kth largest value to find + Returns: + Optional[BinaryTreeNode]: The kth largest node + """ + + # This is a helper class that helps to track the algorithm's progress of traversing the tree + class TreeInfo: + def __init__(self): + self.number_of_nodes_visited: int = 0 + self.latest_visited_node: Optional[BinaryTreeNode] = None + + def reverse_in_order_traverse(node: BinaryTreeNode, tree_information: TreeInfo): + """ + Helper function to traverse the tree in reverse in order + Args: + node (BinaryTreeNode): The node to traverse + tree_information (TreeInfo): The tree information + """ + # base case: if node is None or we've already found the kth largest + if not node or tree_information.number_of_nodes_visited >= k: + return + + # traverse right subtree first for larger values + reverse_in_order_traverse(node.right, tree_information) + + # Visit the current node if we haven't found k nodes yet + if tree_information.number_of_nodes_visited < k: + tree_information.number_of_nodes_visited += 1 + tree_information.latest_visited_node = node + + # traverse the left subtree for smaller values if needed + reverse_in_order_traverse(node.left, tree_information) + + tree_info = TreeInfo() + reverse_in_order_traverse(self.root, tree_info) + return tree_info.latest_visited_node + + def find_closest_value_in_bst(self, target: T) -> Optional[BinaryTreeNode]: + """ + Finds the closest value in the binary search tree to the given target value. + + Args: + target T: Value to search for + Returns: + Node with the closest value to the target + """ + # edge case for empty nodes, if none is provided, we can't find a value that is close to the target + if not self.root: + return None + + # if the node's data is the target, exit early by returning it + if self.root.data == target: + return self.root + + # this keeps track of the minimum on both the left and the right + closest_node = self.root + min_diff = abs(target - self.root.data) + current = self.root + + # while the queue is not empty, we pop off nodes from the queue and check for their values + while current: + current_diff = abs(target - current.data) + + if current_diff < min_diff: + min_diff = current_diff + closest_node = current + + if current.data == target: + return current + + if target < current.data: + current = current.left + else: + current = current.right + + return closest_node + + def range_sum(self, low: int, high: int): + """ + returns the sum of datas of all nodes with a data in the range [low, high]. + + Example: + Input: root = [10,5,15,3,7,null,18], low = 7, high = 15 + Output: 32 + + Uses an iterative approach to solving the problem with Breadth First Search Traversal + + Complexity Analysis: + - Time Complexity: O(N), where N is the number of nodes in the tree + - Space Complexity: O(N) + - The stack will contain no more than 2 levels of the nodes. The maximal number of nodes in a binary tree is + N/2. Therefore, the maximal space needed for the stack is O(N) + """ + + ans = 0 + + stack = [self.root] + + while stack: + node = stack.pop() + + if node: + if low <= node.data <= high: + ans += node.data + + if low < node.data: + if node.left: + stack.append(node.left) + + if node.data < high: + if node.right: + stack.append(node.right) + + return ans + + def breadth_first_search(self) -> List[Any]: + """ + Performs a breadth first search through a Binary Tree + This will traverse the tree level by level and depth by depth. Using a Queue to put elements into the queue + """ + result = [] + queue = FifoQueue() + + # start off by adding the root node + queue.enqueue(self.root) + + # while the queue is not empty, we want to traverse the tree and add elements to the queue, + while not queue.is_empty(): + current_node = queue.dequeue() + result.append(current_node.data) + + if current_node.left: + queue.enqueue(current_node.left) + + if current_node.right: + queue.enqueue(current_node.right) + + return result + + def pre_order(self) -> List[Any]: + """ + Type of Depth First Traversal (DFS) for binary trees which will start at root node and proceed to the left + data and print it until it reaches the leaf(node with no more children) and then backtrack to the node and + check if the current node has a right child and print it. This will continue until all nodes have been + tracked and printed. + """ + result = [] + stack = DynamicSizeStack() + + if not self.root: + return result + + current = self.root + + while current or not stack.is_empty(): + while current: + result.append(current.data) + stack.push(current) + current = current.left + + current = stack.pop() + + current = current.right + + return result + + def increasing_order_traversal(self) -> Optional[BinaryTreeNode]: + if not self.root: + return None + + def inorder(node: BinaryTreeNode): + if node: + yield from inorder(node.left) + yield node.data + yield from inorder(node.right) + + result = current = BinaryTreeNode(None) + for data in inorder(self.root): + current.right = BinaryTreeNode(data) + current = current.right + + return result.right + + def in_order_recurse(self, node: BinaryTreeNode) -> List[T]: + """ + Another type of Depth First Search (DFS) that traverses the tree from the left to middle to right of the tree. + This type of search will begin at the left node and check if that node has a left child and continually check + until that left node is a leaf(has no children) and will then print its data and "bubble up" back to the + current node and execute that (in this case print it) and then print the right node. The same procedure is + executed for the right side of the tree. + """ + result = [] + if self.root: + if self.root.left: + self.in_order_recurse(self.root.left) + + result.append(self.root.data) + + if self.root.right: + self.in_order_recurse(self.root.right) + return result + + def in_order_iterate(self) -> List[T]: + """ + Iterative approach using a stack + """ + result = [] + stack = DynamicSizeStack() + current = self.root + + while current or not stack.is_empty(): + while current: + stack.push(current) + current = current.left + + current = stack.pop() + result.append(current.data) + current = current.right + + return result + + def in_order_morris_traversal(self) -> List[Any]: + result = [] + current = self.root + pre = None + + while current: + if not current.left: + # add the current data of the node + result.append(current.data) + # Move to next right node + current = current.right + else: + # we have a left subtree + pre = current.left + + # find rightmost + while pre.right: + pre = pre.right + + # put current after the pre node + pre.right = current + # store current node + temp = current + # move current to top of new tree + current = current.left + # original current left be None, avoid infinite loops + temp.left = None + + return result + + def post_order(self) -> List[Any]: + """ + 1. Push root to first stack. + 2. Loop while first stack is not empty + 2.1 Pop a node from first stack and push it to second stack + 2.2 Push left and right children of the popped node to first stack + 3. Print contents of second stack + """ + if not self.root: + return [] + + # create 2 stacks + stack_one = DynamicSizeStack() + stack_two = DynamicSizeStack() + data = [] + + # push root to stack one + stack_one.push(self.root) + + # while stack 1 is not empty + while stack_one: + # pop a node from stack 1 and add it to stack 2 + node = stack_one.pop() + stack_two.push(node) + + # push left & right children of removed item to stack one + if node.left: + stack_one.push(node.left) + + if node.right: + stack_one.push(node.right) + + while stack_two: + node = stack_two.pop() + data.append(node.data) + + return data + + def get_depth(self) -> int: + pass + + def level_order_traversal(self) -> List[Any]: + pass + + def pre_order_traversal(self) -> List[Any]: + data = [] + if not self.root: + return data + + def pre_order_helper(root: BinaryTreeNode): + if not root: + return + data.append(root.data) + pre_order_helper(root.left) + pre_order_helper(root.right) + + pre_order_helper(self.root) + return data + + def is_valid(self): + """ + Checks if a binary search tree is valid. A data of None is a valid Binary Search Tree + Complexity Analysis: + Space Complexity: O(n) as we are using a stack to keep track of the nodes, where n is the height of the tree + Time Complexity: O(n), worst case we travers all the nodes of the tree(left sub tree and right sub tree) to + check if the tree is a valid BST + :rtype: bool + :return: Boolean True if valid, False otherwise + """ + + # Tree with no root is still valid + if not self.root: + return True + + # start with the root with an arbitrarily low lower bound and an arbitrarily higher bound + stack = [(float("-inf"), self.root, float("inf"))] + + # depth first traversal + while stack: + lower_bound, node, upper_bound = stack.pop() + + if not node: + continue + + # if this node is invalid, return false immediately + if node.data <= lower_bound or node.data >= upper_bound: + return False + + if node.left: + # this node must be less than the current node + stack.append((lower_bound, node.left, node.data)) + + if node.right: + # this node must be greater than the current node + stack.append((node.data, node.right, upper_bound)) + + # if none of the nodes are invalid, return true + # at this point we have checked all the nodes + return True + + def is_valid_recursive(self): + """ + This uses the call stack to check if the binary search tree node is valid. + This will work, but is vulnerable to stack overflow error + Possible :exception: OverflowError + :return: True/False if the root is a valid binary search tree + :rtype: bool + """ + + if not self.root: + return True + + def is_valid_recursive_helper( + node: BinaryTreeNode, low=-float("inf"), high=float("inf") + ): + """ + This uses the call stack to check if the binary search tree node is valid. + This will work, but is vulnerable to stack overflow error + Possible :exception: OverflowError + :param node: Binary search tree node to check for + :param low: the lower bound set arbitrarily + :param high: upper bound set arbitrarily + :return: True/False if the root is a valid binary search tree + :rtype: bool + """ + + if not node: + return True + + # if the data is out of bounds + if node.data >= high or node.data <= low: + return False + + return not ( + not is_valid_recursive_helper(node.left, low, node.data) + or not is_valid_recursive_helper(node.right, node.data, high) + ) + + lower_bound = float("-inf") + upper_bound = float("inf") + return is_valid_recursive_helper(self.root, lower_bound, upper_bound) + + def search_node(self, data: T) -> bool: + """ + Searches for the given data in a binary search tree. If the data exists in the tree, then True is returned, + else false + Arguments: + data: data to search for + """ + + def search_helper(current: Optional[BinaryTreeNode], value: T) -> bool: + """ + Search helper that is used to search the binary search tree for the given value + Arguments: + current: the current binary search tree node, could be a missing value + value: the value to search for + """ + if current: + if current.data == value: + return True + elif current.data < value: + return search_helper(current.right, value) + else: + return search_helper(current.left, value) + return False + + return search_helper(self.root, data) + + def merge_trees(self, other_node: BinaryTreeNode) -> BinaryTreeNode: + """ + Merges this tree with another tree given another node + :param other_node Other Root node, may be None, therefore we return the root node if availables + :type BinaryTreeNode + :returns Binary Tree Node + """ + if not other_node: + return self.root + + if not self.root: + return other_node + + self.root.data += other_node.data + + self.root.left = self.merge_trees(other_node.left) + self.root.right = self.merge_trees(other_node.right) + + return self.root + + def is_balanced(self) -> bool: + """ + Checks if a binary tree is balanced + :return: True/False, if a binary tree is balanced + :rtype: bool + """ + if self.root is None: + return True + + tree_root = self.root + + # short circuit as soon as we find more than 2 + depths = [] + + # treat this list as a stack, that will store tuples of (node, depth) + # give the stack a maximum size of the length of the tree root + # alternatively, we could use a list + # nodes = [] + nodes = DynamicSizeStack() + + # nodes.append((tree_root, 0)) + nodes.push((tree_root, 0)) + + while len(nodes): + # pop a node and its depth from the top of a stack + node, depth = nodes.pop() + + # case, we found a leaf + if not node.left and not node.right: + # we only care if it is a new depth + if depth not in depths: + depths.append(depth) + + # two ways we might now have an unbalanced tree: + # 1) more than 2 different leaf depths + # 2) 2 leaf depths that are more than 1 apart + if len(depths) > 2 or ( + len(depths) == 2 and abs(depths[0] - depths[1]) > 1 + ): + return False + + # case, this is not a leaf, keep stepping down + else: + if node.left: + nodes.push((node.left, depth + 1)) + if node.right: + nodes.push((node.right, depth + 1)) + + return True + + def lowest_common_ancestor( + self, node_one: BinaryTreeNode, node_two: BinaryTreeNode + ) -> BinaryTreeNode: + """ + Considering it is a BST, we can assume that this tree is a valid BST, we could also check for this + If both of the datas in the 2 nodes provided are greater than the root node, then we move to the right. + if the nodes are less than the root node, we move to the left. + If there is no root node, then we exit and return None, as no common ancestor could exist in such a case with + no root node. + + Assumptions: + - assumes that the node itself can also be an ancestor/descendant of itself + + Complexity Analysis: + + Time Complexity: O(h). + The Time Complexity of the above solution is O(h), where h is the height of the tree. + + Space Complexity: O(1). + The space complexity of the above solution is constant. + """ + + if not self.root: + return None + + # if any of the node datas matches the data data for the root node, return the root node + if self.root.data == node_one.data or self.root.data == node_two.data: + return self.root + + while self.root: + # if both node_one and node_two are smaller than root, then LCA lies in the left + if self.root.data > node_one.data and self.root.data > node_two.data: + self.root = self.root.left + + # if both node_one and node_two are greater than root, then LCA lies in the right + elif self.root.data < node_one.data and self.root.data < node_two.data: + self.root = self.root.right + else: + break + + return self.root + + @property + def paths(self) -> list: + """ + Gets all the paths of this tree from root node to leaf nodes + """ + if not self.root: + return [] + + stack = DynamicSizeStack() + stack.push((self.root, "")) + res = [] + + while stack: + node, path = stack.pop() + + if not (node.left or node.right): + res.append(path + str(node.data)) + + if node.left: + stack.push((node.left, path + str(node.data) + "->")) + + if node.right: + stack.push((node.right, path + str(node.data) + "->")) + + return [list(map(int, x.split("->"))) for x in res] + + def inorder_successor(self, node: BinaryTreeNode) -> Optional[BinaryTreeNode]: + """ + Returns the inorder successor of the node. If there is no node, None is returned. The inorder successor of the + node is the node with the smallest value greater than node.data in the binary search tree. + + This assumes that the node is in the tree already. + + Complexity: + + Time Complexity: The time complexity of this solution is O(n) in the worst-case scenario where the given tree + is skewed. However, for a balanced binary search tree, it will be O(logn). + + Space Complexity: O(1) because we don't use any additional space. + + Args: + node (BinaryTreeNode): node to search for inorder successor + Returns: + Optional[BinaryTreeNode]: returns inorder successor of node if available, else None + """ + if not self.root: + return None + + successor = None + current = self.root + + # current is the best candidate so far + while current: + # when current.data is greater than the node.data, we have found a valid successor, so, we save it in the + # successor variable + if current.data > node.data: + successor = current + # Move left to find a better(smaller) candidate. By moving to current.left, we are exploring values that + # are smaller than current.data. Since we want the smallest possible successor, we must check the left + # side of current to see if there is a node that is still greater than node.data, but close to node.data + current = current.left + else: + # this node is too small, so we must go right to find a larger value + current = current.right + + return successor + + def sum_nodes_in_range(self, low: int, high: int) -> int: + """ + Finds and sums all the nodes in the range [low, high]. Utilizes pruning to avoid checking subtrees whose values + fall outside the range [low, high]. + Args: + low (int): Lower bound. + high (int): Upper bound. + Returns: + int: Total sum of nodes within the range [low, high]. + """ + + def dfs(node: Optional[BinaryTreeNode]): + if not node: + return 0 + if node.data < low: + return dfs(node.right) + if node.data > high: + return dfs(node.left) + return node.data + dfs(node.left) + dfs(node.right) + + return dfs(self.root) diff --git a/datastructures/trees/binary/search_tree/bst_utils.py b/datastructures/trees/binary/search_tree/bst_utils.py new file mode 100644 index 00000000..9381ea15 --- /dev/null +++ b/datastructures/trees/binary/search_tree/bst_utils.py @@ -0,0 +1,30 @@ +from typing import Optional +from datastructures.trees.binary.node import BinaryTreeNode +from datastructures.trees import T + + +def is_valid_bst(root: Optional[BinaryTreeNode]) -> bool: + """ + Checks if a binary search tree is valid. + Args: + root(BinaryTreeNode): The root of the binary search tree. + Returns: + bool: True if the binary search tree is valid, False otherwise. + """ + if not root: + return True + + def dfs(node: Optional[BinaryTreeNode], min_value: T, max_value: T) -> bool: + if not node: + return True + + # Is the current node's value within the given range? + if node.data <= min_value or node.data >= max_value: + # If not, return False immediately + return False + + return dfs(node.left, min_value, node.data) and dfs( + node.right, node.data, max_value + ) + + return dfs(root, float("-inf"), float("inf")) diff --git a/datastructures/trees/binary/search_tree/images/examples/is_valid_bst_example_1.png b/datastructures/trees/binary/search_tree/images/examples/is_valid_bst_example_1.png new file mode 100644 index 00000000..657bad06 Binary files /dev/null and b/datastructures/trees/binary/search_tree/images/examples/is_valid_bst_example_1.png differ diff --git a/datastructures/trees/binary/search_tree/images/examples/is_valid_bst_example_2.png b/datastructures/trees/binary/search_tree/images/examples/is_valid_bst_example_2.png new file mode 100644 index 00000000..2d788e02 Binary files /dev/null and b/datastructures/trees/binary/search_tree/images/examples/is_valid_bst_example_2.png differ diff --git a/datastructures/trees/binary/search_tree/images/solutions/is_valid_bst_solution_1.png b/datastructures/trees/binary/search_tree/images/solutions/is_valid_bst_solution_1.png new file mode 100644 index 00000000..e55c78ee Binary files /dev/null and b/datastructures/trees/binary/search_tree/images/solutions/is_valid_bst_solution_1.png differ diff --git a/datastructures/trees/binary/search_tree/images/solutions/is_valid_bst_solution_2.png b/datastructures/trees/binary/search_tree/images/solutions/is_valid_bst_solution_2.png new file mode 100644 index 00000000..a3f585a2 Binary files /dev/null and b/datastructures/trees/binary/search_tree/images/solutions/is_valid_bst_solution_2.png differ diff --git a/datastructures/trees/binary/search_tree/images/solutions/is_valid_bst_solution_3.png b/datastructures/trees/binary/search_tree/images/solutions/is_valid_bst_solution_3.png new file mode 100644 index 00000000..dc0d28d4 Binary files /dev/null and b/datastructures/trees/binary/search_tree/images/solutions/is_valid_bst_solution_3.png differ diff --git a/datastructures/trees/binary/search_tree/images/solutions/is_valid_bst_solution_4.png b/datastructures/trees/binary/search_tree/images/solutions/is_valid_bst_solution_4.png new file mode 100644 index 00000000..f62f0bdc Binary files /dev/null and b/datastructures/trees/binary/search_tree/images/solutions/is_valid_bst_solution_4.png differ diff --git a/datastructures/trees/binary/search_tree/test_binary_search_tree_valid.py b/datastructures/trees/binary/search_tree/test_binary_search_tree_valid.py new file mode 100644 index 00000000..560deef3 --- /dev/null +++ b/datastructures/trees/binary/search_tree/test_binary_search_tree_valid.py @@ -0,0 +1,48 @@ +import unittest +from typing import List +from parameterized import parameterized +from datastructures.trees.binary.search_tree import BinarySearchTree +from datastructures.trees.binary.search_tree.bst_utils import is_valid_bst + +IS_VALID_BST_TEST_CASES = [ + ([8, 3, 10, 1, 6], True), + ([2, 1, 4], True), + ([1, None, 1], False), + ([6, 2, 8, None, None, 7, 11], True), +] + + +class BinarySearchTreeIsValidTestCases(unittest.TestCase): + @parameterized.expand(IS_VALID_BST_TEST_CASES) + def test_is_valid(self, data: List[int], expected: bool): + search_tree = BinarySearchTree() + for d in data: + search_tree.insert_node(d) + + actual = search_tree.is_valid() + + self.assertEqual(expected, actual) + + @parameterized.expand(IS_VALID_BST_TEST_CASES) + def test_is_valid_recursive(self, data: List[int], expected: bool): + search_tree = BinarySearchTree() + for d in data: + search_tree.insert_node(d) + + actual = search_tree.is_valid_recursive() + + self.assertEqual(expected, actual) + + @parameterized.expand(IS_VALID_BST_TEST_CASES) + def test_is_valid_util(self, data: List[int], expected: bool): + search_tree = BinarySearchTree() + for d in data: + search_tree.insert_node(d) + + actual = is_valid_bst(search_tree.root) + + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/datastructures/trees/binary/test_utils.py b/datastructures/trees/binary/test_utils.py index 0f470021..f729c7e6 100644 --- a/datastructures/trees/binary/test_utils.py +++ b/datastructures/trees/binary/test_utils.py @@ -9,7 +9,7 @@ mirror_binary_tree, ) from datastructures.trees.binary.node import BinaryTreeNode -from datastructures.trees.binary.tree.tree_utils import ( +from datastructures.trees.binary.tree.binary_tree_utils import ( create_tree_from_nodes, level_order_traversal, ) diff --git a/datastructures/trees/binary/tree/README.md b/datastructures/trees/binary/tree/README.md index 85e637f0..62b5463f 100644 --- a/datastructures/trees/binary/tree/README.md +++ b/datastructures/trees/binary/tree/README.md @@ -453,3 +453,24 @@ The time complexity of this solution is linear O(n), where n is the number of no The space complexity of this solution is O(h), where h is the height of the tree. This is because our recursive algorithm uses space on the call stack, which can grow to the height of the binary tree. The complexity will be O(log(n)) for a balanced tree and O(n) for a degenerate tree. + +--- + +## Longest Univalue Path + +Given the root of the binary tree, find the longest path where all nodes along the path have the same value. This path +doesn't have to include the root node. Return the number of edges on that path, not the number of nodes. + +### Examples + +![Example Longest univalue path 1](./images/examples/longest_univalue_path_example_1.png) + +Input: [1,4,5,4,4,5] +Output: 2 +Explanation: The longest path of the same value is the path [4,4,4], which has a total of 2 edges. + +![Example Longest univalue path 2](./images/examples/longest_univalue_path_example_2.png) + +Input: [1,1,1,1,1,1,1] +Output: 4 +Explanation: The longest path of the same value is the path [1,1,1,1,1], which has a length of 4. diff --git a/datastructures/trees/binary/tree/binary_tree.py b/datastructures/trees/binary/tree/binary_tree.py index b4e7562f..5418f41c 100644 --- a/datastructures/trees/binary/tree/binary_tree.py +++ b/datastructures/trees/binary/tree/binary_tree.py @@ -6,6 +6,7 @@ from datastructures.stacks.dynamic import DynamicSizeStack from datastructures.trees import Tree, TreeNode, T from datastructures.trees.binary.node import BinaryTreeNode +from datastructures.trees.binary.tree.binary_tree_utils import longest_uni_value_path from datastructures.queues.fifo import FifoQueue @@ -933,3 +934,29 @@ def invert_tree_helper( return node return invert_tree_helper(self.root) + + def sum_nodes_in_range(self, low: int, high: int) -> int: + """ + Finds and sums all the nodes in the range [low, high]. + Args: + low (int): Lower bound. + high (int): Upper bound. + Returns: + int: Total sum of nodes within the range [low, high]. + """ + + def dfs(node: Optional[BinaryTreeNode]): + if not node or node.data < low or node.data > high: + return 0 + return node.data + dfs(node.left) + dfs(node.right) + + return dfs(self.root) + + def longest_uni_value_path(self) -> int: + """ + Returns the length of the longest path, where each node in the path has the same value. This path may or may + not pass through the root. + + The length of the path between two nodes is represented by the number of edges between them. + """ + return longest_uni_value_path(self.root) diff --git a/datastructures/trees/binary/tree/tree_utils.py b/datastructures/trees/binary/tree/binary_tree_utils.py similarity index 70% rename from datastructures/trees/binary/tree/tree_utils.py rename to datastructures/trees/binary/tree/binary_tree_utils.py index fa83a488..4a240a23 100644 --- a/datastructures/trees/binary/tree/tree_utils.py +++ b/datastructures/trees/binary/tree/binary_tree_utils.py @@ -70,3 +70,34 @@ def level_order_traversal(root: Optional[BinaryTreeNode]) -> List[Any]: current_level = next_level return list(chain.from_iterable(levels)) + + +def longest_uni_value_path(root: Optional[BinaryTreeNode]) -> int: + """ + Returns the length of the longest path, where each node in the path has the same value. This path may or may + not pass through the root. + + The length of the path between two nodes is represented by the number of edges between them. + """ + max_length = 0 + + def dfs(node: Optional[BinaryTreeNode]) -> int: + nonlocal max_length + + if not node: + return 0 + + left_value = dfs(node.left) + right_value = dfs(node.right) + + left_arrow, right_arrow = 0, 0 + if node.left and node.left.data == node.data: + left_arrow = left_value + 1 + if node.right and node.right.data == node.data: + right_arrow = right_value + 1 + + max_length = max(max_length, left_arrow + right_arrow) + return max(left_arrow, right_arrow) + + dfs(root) + return max_length diff --git a/datastructures/trees/binary/tree/images/examples/longest_univalue_path_example_1.png b/datastructures/trees/binary/tree/images/examples/longest_univalue_path_example_1.png new file mode 100644 index 00000000..d0897891 Binary files /dev/null and b/datastructures/trees/binary/tree/images/examples/longest_univalue_path_example_1.png differ diff --git a/datastructures/trees/binary/tree/images/examples/longest_univalue_path_example_2.png b/datastructures/trees/binary/tree/images/examples/longest_univalue_path_example_2.png new file mode 100644 index 00000000..86c2e138 Binary files /dev/null and b/datastructures/trees/binary/tree/images/examples/longest_univalue_path_example_2.png differ diff --git a/pymath/self_crossing/README.md b/pymath/self_crossing/README.md new file mode 100644 index 00000000..f35609f7 --- /dev/null +++ b/pymath/self_crossing/README.md @@ -0,0 +1,127 @@ +# Self Crossing +You are given an array of integers, distance, where each element represents the length of a move you will make on an +X-Y plane. You start at the origin, which is point (0,0), and move according to the array. Specifically, you move +distance[0] meters north, distance[1] meters west, distance[2] meters south, distance[3] meters east, and continue this +pattern in a counterclockwise direction. Each step follows the sequence—north, west, south, east—repeating as long as +there are remaining distances in the array. + +Your task is to determine whether this path crosses itself at any point. This means checking whether you revisit any +previously visited position (including the origin or any other point) at any step. Return TRUE if the path intersects +itself, and FALSE otherwise. + +## Constraints + +- 1 ≤ `distance.length` ≤ 10^5 +- 1 ≤ `distance[i]` ≤ 10^5 + +## Examples + +![Example 1](./images/examples/is_self_crossing_example_1.png) + +> Input: distance = [2,1,1,2] +> Output: true +> Explanation: The path crosses itself at the point (0, 1). + +![Example 2](./images/examples/is_self_crossing_example_2.png) + +> Input: distance = [1,2,3,4] +> Output: false +> Explanation: The path does not cross itself at any point. + +![Example 3](./images/examples/is_self_crossing_example_3.png) + +> Input: distance = [1,1,1,2,1] +> Output: true +> Explanation: The path crosses itself at the point (0, 0). + +## Topics + +- Array +- Math +- Geometry + +## Solution + +The essence of this solution lies in recognizing the geometric patterns formed when a path moves sequentially in a +structured, counterclockwise manner—north, west, south, and east. This problem follows the math and geometry coding +pattern, where mathematical conditions detect self-crossings based on movement distances alone rather than explicitly +tracking coordinates. + +Instead of maintaining a coordinate system, we analyze movement distances and use mathematical comparisons to determine +if the path intersects itself. The key insight is that self crossing occurs when a new line overlaps or intersects a +previous one. By iterating through the distance list, we identify specific conditions used to dynamically handle any +path length effectively. This approach ensures scalability and optimal efficiency. + +Specifically, there are three primary cases where self crossing might initially occur, each described below. + +> Note: While these cases describe initial intersections involving the 4th, 5th, and 6th lines explicitly, the +> conditions themselves are dynamically applied at every subsequent step for longer paths: + +- **Fourth line crosses the first**: This scenario occurs when the current step (distance[i]) crosses over the line from two + steps before (distance[i - 2]), and the previous step (distance[i -1]) is less than or equal to the distance of three + steps back (distance[i - 3]). + + > distance[i] ≥ distance[i−2] and distance[i−1] ≤ distance[i−3] + +- Fifth line meets first (Overlap): This occurs when the fifth line aligns exactly with, overlaps, or just touches the + point at which the first line began (0,0). Even touching the origin at the end of the fifth line segment counts as + self crossing. The condition checks if the previous step distance[i - 1] equals the step three steps back + distance[i - 3] and whether the sum of the current step distance[i] and the step four steps back distance[i - 4] is + greater than or equal to the step two steps back distance[i - 2]. + + > distance[i−1] == distance[i−3] and distance[i] + distance[i−4] ≥ distance[i−2] + +- **Sixth line crosses first (spiral crossing)**: This is a more complex scenario where the sixth line crosses back over the + first, forming a spiral pattern. Several conditions must simultaneously be met: + - The fourth step is greater than or equal to the second step: + + > distance[i−2] ≥ distance[i−4] + + - The fifth step is less than or equal to the third step: + + > distance[i−1] ≤ distance[i−3] + + - The sum of the first and fifth steps is greater than or equal to the third step: + + > distance[i−1] + distance[i−5] ≥ distance[i−3] + + - The sum of the current (sixth) step and the fourth step is greater than or equal to the second step: + + > distance[i] + distance[i−4] ≥ distance[i−2] + +This combination ensures the path spirals inward enough to intersect the initial segment. + +Now, let’s look at the solution steps below: + +1. **Base condition**: If the input list has fewer than four elements, return FALSE because crossing itself with three or + fewer moves is impossible. +2. **Iterate through the list**: Start iterating from index 3 to check for possible crossings. +3. **Check for crossing cases**: + - **The fourth line crosses the first (Case 1)**: The fourth step overlaps with the first if the current step is + greater than or equal to two steps back (distance[i] >= distance[i - 2]), and the previous step is less than or + equal to three steps back (distance[i - 1] <= distance[i - 3]) + - **The fifth line meets first (Overlap—Case 2)**: The fifth step aligns with the first if the previous step equals + the step three steps back (distance[i - 1] == distance[i - 3]), and the sum of the current step and the step four + steps back is greater than or equal to the step two steps back (distance[i] >= distance[i - 2] - distance[i - 4]). + - **The sixth line crosses first (Spiral crossing—Case 3)**: This happens if: + - The fourth step is greater than or equal to the second step (distance[i - 2] >= distance[i - 4]). + - The fifth step is less than or equal to the third step (distance[i - 1] <= distance[i - 3]). + - The sum of the current step and the fourth step is greater than or equal to the second step (distance[i] + distance[i - 4] >= distance[i - 2]). + - The sum of the fifth step and the first step is greater than or equal to the third step (distance[i - 1] + distance[i - 5] >= distance[i - 3]). +4. If any of the above conditions are met, return TRUE. If the loop completes without detecting a crossing, return FALSE. + +Let’s look at the following illustration to get a better understanding of the solution: + +![Solution 1](./images/solutions/is_self_crossing_solution_1.png) +![Solution 2](./images/solutions/is_self_crossing_solution_2.png) +![Solution 3](./images/solutions/is_self_crossing_solution_3.png) + +### Time Complexity + +The time complexity of the code is `O(n)` because it iterates through the distance array once, where n is the length of +the distance array. + +### Space Complexity + +The space complexity of the code is O(1) because it stores the variables in a fixed amount of space and does not use +additional data structures that scale with the input size. diff --git a/pymath/self_crossing/__init__.py b/pymath/self_crossing/__init__.py new file mode 100644 index 00000000..34a6dc74 --- /dev/null +++ b/pymath/self_crossing/__init__.py @@ -0,0 +1,41 @@ +from typing import List + + +def is_self_crossing(distance: List[int]) -> bool: + # If there are fewer than four moves, crossing is impossible + if len(distance) < 4: + return False + + # Iterate through the distance list starting from index 3 + for i in range(3, len(distance)): + # Case 1: Fourth line crosses the first line + # This happens when the current distance is greater than or equal to the distance two steps back, + # and the previous step is less than or equal to the step three steps back. + if distance[i] >= distance[i - 2] and distance[i - 3] >= distance[i - 1]: + return True + + # Case 2: Fifth line meets the first line (overlap) + # This occurs when the previous step equals the step three steps back, + # and the sum of the current step and the step four steps back is greater than or equal to the step two steps back. + if ( + i >= 4 + and distance[i - 1] == distance[i - 3] + and distance[i] >= distance[i - 2] - distance[i - 4] + ): + return True + + # Case 3: Sixth line crosses the first line (spiral crossing) + # This happens when: + # - The fourth step is greater than or equal to the second step. + # - The sum of the current step and the fourth step is greater than or equal to the second step. + # - The fifth step is less than or equal to the third step. + # - The sum of the fifth step and the first step is greater than or equal to the third step. + if ( + i >= 5 + and distance[i - 4] <= distance[i - 2] <= distance[i] + distance[i - 4] + and distance[i - 1] <= distance[i - 3] <= distance[i - 1] + distance[i - 5] + ): + return True + + # If no crossing is detected, return False + return False diff --git a/pymath/self_crossing/images/examples/is_self_crossing_example_1.png b/pymath/self_crossing/images/examples/is_self_crossing_example_1.png new file mode 100644 index 00000000..96593fc4 Binary files /dev/null and b/pymath/self_crossing/images/examples/is_self_crossing_example_1.png differ diff --git a/pymath/self_crossing/images/examples/is_self_crossing_example_2.png b/pymath/self_crossing/images/examples/is_self_crossing_example_2.png new file mode 100644 index 00000000..b8b863ad Binary files /dev/null and b/pymath/self_crossing/images/examples/is_self_crossing_example_2.png differ diff --git a/pymath/self_crossing/images/examples/is_self_crossing_example_3.png b/pymath/self_crossing/images/examples/is_self_crossing_example_3.png new file mode 100644 index 00000000..fbd4de4f Binary files /dev/null and b/pymath/self_crossing/images/examples/is_self_crossing_example_3.png differ diff --git a/pymath/self_crossing/images/solutions/is_self_crossing_solution_1.png b/pymath/self_crossing/images/solutions/is_self_crossing_solution_1.png new file mode 100644 index 00000000..fe2f8870 Binary files /dev/null and b/pymath/self_crossing/images/solutions/is_self_crossing_solution_1.png differ diff --git a/pymath/self_crossing/images/solutions/is_self_crossing_solution_2.png b/pymath/self_crossing/images/solutions/is_self_crossing_solution_2.png new file mode 100644 index 00000000..ed13751e Binary files /dev/null and b/pymath/self_crossing/images/solutions/is_self_crossing_solution_2.png differ diff --git a/pymath/self_crossing/images/solutions/is_self_crossing_solution_3.png b/pymath/self_crossing/images/solutions/is_self_crossing_solution_3.png new file mode 100644 index 00000000..2a536005 Binary files /dev/null and b/pymath/self_crossing/images/solutions/is_self_crossing_solution_3.png differ diff --git a/pymath/self_crossing/test_is_self_crossing.py b/pymath/self_crossing/test_is_self_crossing.py new file mode 100644 index 00000000..c6e0bf65 --- /dev/null +++ b/pymath/self_crossing/test_is_self_crossing.py @@ -0,0 +1,24 @@ +import unittest +from typing import List +from parameterized import parameterized +from pymath.self_crossing import is_self_crossing + +IS_SELF_CROSSING_TEST_CASES = [ + ([2, 1, 1, 2], True), + ([1, 1, 2, 1, 1], True), + ([1, 2, 3, 4], False), + ([1, 1, 2, 2, 1, 1], True), + ([1, 1, 1, 2, 1], True), + ([1, 1, 2, 2, 3, 3, 4, 4, 10, 4, 4, 3, 3, 2, 2, 1, 1], False), +] + + +class IsSelfCrossingTestCase(unittest.TestCase): + @parameterized.expand(IS_SELF_CROSSING_TEST_CASES) + def test_is_self_crossing(self, distance: List[int], expected: bool): + actual = is_self_crossing(distance) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/algorithms/sliding_window/__init__.py b/tests/algorithms/sliding_window/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/algorithms/sliding_window/test_min_window_substring.py b/tests/algorithms/sliding_window/test_min_window_substring.py deleted file mode 100644 index db86e94b..00000000 --- a/tests/algorithms/sliding_window/test_min_window_substring.py +++ /dev/null @@ -1,32 +0,0 @@ -import unittest -from algorithms.sliding_window.minimum_window_substring import min_window - - -class MinWindowSubstringTestCase(unittest.TestCase): - def test_s_is_ADOBECODEBANC_and_t_is_ABC(self): - """s=ADOBECODEBANC and t = ABC should return BANC""" - s = "ADOBECODEBANC" - t = "ABC" - expected = "BANC" - actual = min_window(s, t) - self.assertEqual(expected, actual) - - def test_s_is_a_and_t_is_a(self): - """s=a and t = a should return a""" - s = "a" - t = "a" - expected = "a" - actual = min_window(s, t) - self.assertEqual(expected, actual) - - def test_s_is_a_and_t_is_aa(self): - """s=a and t = aa should return ''""" - s = "a" - t = "aa" - expected = "" - actual = min_window(s, t) - self.assertEqual(expected, actual) - - -if __name__ == "__main__": - unittest.main()