-
Notifications
You must be signed in to change notification settings - Fork 2
feat(algorithms, dynamic-programming): minimum path sum in a triangle #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
algorithms/max_path_sum/README.md → ...ynamic_programming/max_path_sum/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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] |
31 changes: 31 additions & 0 deletions
31
algorithms/dynamic_programming/min_path_sum/test_min_path_sum.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.