diff --git a/DIRECTORY.md b/DIRECTORY.md index 7ef5db18..5229a424 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -66,6 +66,8 @@ * [Test Longest Common Subsequence](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/longest_common_subsequence/test_longest_common_subsequence.py) * Min Distance * [Test Min Distance](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/min_distance/test_min_distance.py) + * Min Path Sum + * [Test Min Path Sum](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/min_path_sum/test_min_path_sum.py) * Unique Paths * [Test Unique Paths](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/unique_paths/test_unique_paths.py) * Word Break @@ -250,6 +252,7 @@ * [Test Doubly Linked List](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/doubly_linked_list/test_doubly_linked_list.py) * [Test Doubly Linked List Move Tail To Head](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/doubly_linked_list/test_doubly_linked_list_move_tail_to_head.py) * [Test Doubly Linked List Palindrome](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/doubly_linked_list/test_doubly_linked_list_palindrome.py) + * [Linked List Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/linked_list_utils.py) * Mergeklinkedlists * [Test Merge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/mergeklinkedlists/test_merge.py) * Singly Linked List diff --git a/algorithms/max_path_sum/README.md b/algorithms/dynamic_programming/max_path_sum/README.md similarity index 96% rename from algorithms/max_path_sum/README.md rename to algorithms/dynamic_programming/max_path_sum/README.md index ad460d1a..da80131c 100644 --- a/algorithms/max_path_sum/README.md +++ b/algorithms/dynamic_programming/max_path_sum/README.md @@ -1,3 +1,5 @@ +# Max Path Sum in Triangle + By starting at the top of the triangle below and moving to adjacent numbers on the row below, the maximum total from top to bottom is 23. diff --git a/algorithms/max_path_sum/__init__.py b/algorithms/dynamic_programming/max_path_sum/__init__.py similarity index 94% rename from algorithms/max_path_sum/__init__.py rename to algorithms/dynamic_programming/max_path_sum/__init__.py index f3bc142b..5727c4cc 100644 --- a/algorithms/max_path_sum/__init__.py +++ b/algorithms/dynamic_programming/max_path_sum/__init__.py @@ -2,8 +2,10 @@ Finds the maximum path sum in a given triangle of numbers """ +from typing import List -def max_path_sum_in_triangle(triangle): + +def max_path_sum_in_triangle(triangle: List[List[int]]) -> int: """ Finds the maximum sum path in a triangle(tree) and returns it :param triangle: diff --git a/algorithms/max_path_sum/triangle.txt b/algorithms/dynamic_programming/max_path_sum/triangle.txt similarity index 100% rename from algorithms/max_path_sum/triangle.txt rename to algorithms/dynamic_programming/max_path_sum/triangle.txt diff --git a/algorithms/dynamic_programming/min_path_sum/README.md b/algorithms/dynamic_programming/min_path_sum/README.md new file mode 100644 index 00000000..a4d34f87 --- /dev/null +++ b/algorithms/dynamic_programming/min_path_sum/README.md @@ -0,0 +1,55 @@ +# Min Path Sum in Triangle + +Given an array, triangle, return the minimum path sum from top to bottom. You may move to an adjacent number in the row +below at each step. More formally, if you are at index i in the current row, you may move to either index i or index +i+1 in the next row. + +## Constraints + +- 1 <= triangle.length <= 200 +- triangle[0].length == 1 +- triangle[i].length == triangle[i - 1].length + 1 +- -10^4 <= triangle[i][j] <= 10^4 + +## Solution + +The goal is to find the minimum path sum from the top of a triangular array to its base, moving at each step to one of +the two adjacent numbers in the next row. A greedy choice at the top may fail because a small value early on can lead to +a costly region later, and plain recursion is inefficient since it revisits the same subproblems many times. This +problem is a natural fit for dynamic programming: +the best path through a cell depends only on the best paths beneath it (optimal substructure), and many different routes +share the same suffixes (overlapping subproblems). + +To solve it efficiently, we take a bottom-up approach. We begin with the last row, whose values represent the known +final costs of any path ending there. From there, we move upward one row at a time. For each number, we determine its +minimum path cost by adding its own value to the smaller of the two pre-calculated path costs in the row directly below +it. This process effectively “folds” the triangle’s path information upward, continuously updating each row with the +optimal costs from the level below. By this process’s peak, the single top number has been transformed to hold the total +of the most efficient path through the entire structure. + +The following steps can be performed to implement the algorithm above: + +1. First, we create a one-dimensional list, `dp`, to store our minimum path sums. This list is initialized as a copy of + the last row of the triangle, i.e., triangle[-1]. This serves as our base case, because the minimum path cost from + any number in the last row to the bottom is simply its own value. +2. Next, we iterate from the second-to-last row (rowIdx = len(triangle) - 2) of the triangle and move upward, one row at + a time, until we reach the top (rowIdx = 0). This bottom-up order ensures that when we process a row, the optimal + path costs for the row below it are already calculated and stored in the dp list. + - We create a nested loop inside the main loop that iterates through each number in the current row. + - For the current number, triangle[rowIdx][colIdx], we calculate its minimum path sum using: + - min(dp[colIdx], dp[colIdx + 1]) + - After finding the minimum of the two, we add it to triangle[rowIdx][colIdx]. + - Finally, we update the dp list at the current position with this new, smaller total. + +3. After the loops complete, the dp list contains the fully collapsed path information. The final answer for the entire + journey, from the top to the bottom, is now the list’s first element, dp[0]. + +### Time Complexity + +The time complexity is O(n^2) where n is the number of rows in the triangle. This is because the algorithm processes +each element exactly once, and the total number of elements in a triangle with n rows is about n * (n + 1)/2, which +grows quadratically. + +### Space Complexity + +The space complexity is O(n) since we only maintain a single working array `dp` with one entry per row. diff --git a/algorithms/dynamic_programming/min_path_sum/__init__.py b/algorithms/dynamic_programming/min_path_sum/__init__.py new file mode 100644 index 00000000..60a640de --- /dev/null +++ b/algorithms/dynamic_programming/min_path_sum/__init__.py @@ -0,0 +1,62 @@ +from typing import List + + +def min_path_sum(triangle: List[List[int]]) -> int: + """ + Finds the minimum path sum in a given triangle of numbers + This uses bottom up dynamic programming by starting at the bottom, we ensure that every decision made at a higher row + is based on the perfect knowledge of the best possible paths below it + + Complexity: + Time Complexity results in O(n^2), since we visit every number in the triangle exactly once. For n rows, there are + roughtl n^2/2 elements. + Space Complexity is O(1) since the triangle input array is updated in place + + Args: + triangle(list): A list of lists of integers representing the triangle + Returns: + The minimum path sum in the triangle + """ + row_count = len(triangle) + + # start from the second to last row, since the bottom row has no children to begin with + for row in range(row_count - 2, -1, -1): + # ensures that we visit every element for the current row + for col in range(row + 1): + # Each cell is updated to include the minimum path sum from the row below + triangle[row][col] += min( + triangle[row + 1][col], triangle[row + 1][col + 1] + ) + + # the result trickles to the apex + return triangle[0][0] + + +def min_path_sum_2(triangle: List[List[int]]) -> int: + """ + Finds the minimum path sum in a given triangle of numbers + This uses bottom up dynamic programming by starting at the bottom, we ensure that every decision made at a higher row + is based on the perfect knowledge of the best possible paths below it + + Complexity: + Time Complexity results in O(n^2), since we visit every number in the triangle exactly once. For n rows, there are + roughtl n^2/2 elements. + Space Complexity is O(n) since the triangle input array's last row is copied over + + Args: + triangle(list): A list of lists of integers representing the triangle + Returns: + The minimum path sum in the triangle + """ + dp = triangle[-1][:] + row_count = len(triangle) + + # start from the second to last row, since the bottom row has no children to begin with + for row_idx in range(row_count - 2, -1, -1): + # ensures that we visit every element for the current row + for col_idx in range(row_idx + 1): + # Each cell is updated to include the minimum path sum from the row below + dp[col_idx] = triangle[row_idx][col_idx] + min(dp[col_idx], dp[col_idx + 1]) + + # the result trickles to the apex + return dp[0] diff --git a/algorithms/dynamic_programming/min_path_sum/test_min_path_sum.py b/algorithms/dynamic_programming/min_path_sum/test_min_path_sum.py new file mode 100644 index 00000000..24ed67a6 --- /dev/null +++ b/algorithms/dynamic_programming/min_path_sum/test_min_path_sum.py @@ -0,0 +1,31 @@ +import unittest +import copy +from typing import List +from parameterized import parameterized +from algorithms.dynamic_programming.min_path_sum import min_path_sum, min_path_sum_2 + +TEST_CASES = [ + ([[5]], 5), + ([[2], [3, 4]], 5), + ([[1000], [2000, 3000]], 3000), + ([[2], [3, 4], [6, 5, 7], [4, 1, 8, 3]], 11), + ([[7], [3, 8], [8, 1, 0], [2, 7, 4, 4], [4, 5, 2, 6, 5]], 17), +] + + +class MinPathSumInTriangleTestCase(unittest.TestCase): + @parameterized.expand(TEST_CASES) + def test_min_path_sum_in_triangle(self, triangle: List[List[int]], expected: int): + input_triangle = copy.deepcopy(triangle) + actual = min_path_sum(input_triangle) + self.assertEqual(expected, actual) + + @parameterized.expand(TEST_CASES) + def test_min_path_sum_2_in_triangle(self, triangle: List[List[int]], expected: int): + input_triangle = copy.deepcopy(triangle) + actual = min_path_sum_2(input_triangle) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/datastructures/linked_lists/linked_list_utils.py b/datastructures/linked_lists/linked_list_utils.py new file mode 100644 index 00000000..015f336e --- /dev/null +++ b/datastructures/linked_lists/linked_list_utils.py @@ -0,0 +1,21 @@ +from typing import Optional +from datastructures.linked_lists import Node + + +def find_middle_node(head: Optional[Node]) -> Optional[Node]: + """ + Traverse the linked list to find the middle node + Time Complexity: O(n) where n is the number of nodes in the linked list + Space Complexity: O(1) as constant extra space is needed + @return: Middle Node or None + """ + if not head: + return None + + fast_pointer, slow_pointer = head, head + + while fast_pointer and fast_pointer.next: + slow_pointer = slow_pointer.next + fast_pointer = fast_pointer.next.next + + return slow_pointer