Skip to content

Commit dbac2b4

Browse files
authored
Merge pull request #123 from BrianLusina/feat/algorithms-triangle
feat(algorithms, dynamic-programming): minimum path sum in a triangle
2 parents 4d9387b + 8ebedca commit dbac2b4

File tree

8 files changed

+177
-1
lines changed

8 files changed

+177
-1
lines changed

DIRECTORY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
* [Test Longest Common Subsequence](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/longest_common_subsequence/test_longest_common_subsequence.py)
6767
* Min Distance
6868
* [Test Min Distance](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/min_distance/test_min_distance.py)
69+
* Min Path Sum
70+
* [Test Min Path Sum](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/min_path_sum/test_min_path_sum.py)
6971
* Unique Paths
7072
* [Test Unique Paths](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/unique_paths/test_unique_paths.py)
7173
* Word Break
@@ -250,6 +252,7 @@
250252
* [Test Doubly Linked List](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/doubly_linked_list/test_doubly_linked_list.py)
251253
* [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)
252254
* [Test Doubly Linked List Palindrome](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/doubly_linked_list/test_doubly_linked_list_palindrome.py)
255+
* [Linked List Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/linked_list_utils.py)
253256
* Mergeklinkedlists
254257
* [Test Merge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/mergeklinkedlists/test_merge.py)
255258
* Singly Linked List

algorithms/max_path_sum/README.md renamed to algorithms/dynamic_programming/max_path_sum/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Max Path Sum in Triangle
2+
13
By starting at the top of the triangle below and moving to adjacent numbers on the row below, the maximum total from top
24
to bottom is 23.
35

algorithms/max_path_sum/__init__.py renamed to algorithms/dynamic_programming/max_path_sum/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
Finds the maximum path sum in a given triangle of numbers
33
"""
44

5+
from typing import List
56

6-
def max_path_sum_in_triangle(triangle):
7+
8+
def max_path_sum_in_triangle(triangle: List[List[int]]) -> int:
79
"""
810
Finds the maximum sum path in a triangle(tree) and returns it
911
:param triangle:
File renamed without changes.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Min Path Sum in Triangle
2+
3+
Given an array, triangle, return the minimum path sum from top to bottom. You may move to an adjacent number in the row
4+
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
5+
i+1 in the next row.
6+
7+
## Constraints
8+
9+
- 1 <= triangle.length <= 200
10+
- triangle[0].length == 1
11+
- triangle[i].length == triangle[i - 1].length + 1
12+
- -10^4 <= triangle[i][j] <= 10^4
13+
14+
## Solution
15+
16+
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
17+
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
18+
a costly region later, and plain recursion is inefficient since it revisits the same subproblems many times. This
19+
problem is a natural fit for dynamic programming:
20+
the best path through a cell depends only on the best paths beneath it (optimal substructure), and many different routes
21+
share the same suffixes (overlapping subproblems).
22+
23+
To solve it efficiently, we take a bottom-up approach. We begin with the last row, whose values represent the known
24+
final costs of any path ending there. From there, we move upward one row at a time. For each number, we determine its
25+
minimum path cost by adding its own value to the smaller of the two pre-calculated path costs in the row directly below
26+
it. This process effectively “folds” the triangle’s path information upward, continuously updating each row with the
27+
optimal costs from the level below. By this process’s peak, the single top number has been transformed to hold the total
28+
of the most efficient path through the entire structure.
29+
30+
The following steps can be performed to implement the algorithm above:
31+
32+
1. First, we create a one-dimensional list, `dp`, to store our minimum path sums. This list is initialized as a copy of
33+
the last row of the triangle, i.e., triangle[-1]. This serves as our base case, because the minimum path cost from
34+
any number in the last row to the bottom is simply its own value.
35+
2. Next, we iterate from the second-to-last row (rowIdx = len(triangle) - 2) of the triangle and move upward, one row at
36+
a time, until we reach the top (rowIdx = 0). This bottom-up order ensures that when we process a row, the optimal
37+
path costs for the row below it are already calculated and stored in the dp list.
38+
- We create a nested loop inside the main loop that iterates through each number in the current row.
39+
- For the current number, triangle[rowIdx][colIdx], we calculate its minimum path sum using:
40+
- min(dp[colIdx], dp[colIdx + 1])
41+
- After finding the minimum of the two, we add it to triangle[rowIdx][colIdx].
42+
- Finally, we update the dp list at the current position with this new, smaller total.
43+
44+
3. After the loops complete, the dp list contains the fully collapsed path information. The final answer for the entire
45+
journey, from the top to the bottom, is now the list’s first element, dp[0].
46+
47+
### Time Complexity
48+
49+
The time complexity is O(n^2) where n is the number of rows in the triangle. This is because the algorithm processes
50+
each element exactly once, and the total number of elements in a triangle with n rows is about n * (n + 1)/2, which
51+
grows quadratically.
52+
53+
### Space Complexity
54+
55+
The space complexity is O(n) since we only maintain a single working array `dp` with one entry per row.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from typing import List
2+
3+
4+
def min_path_sum(triangle: List[List[int]]) -> int:
5+
"""
6+
Finds the minimum path sum in a given triangle of numbers
7+
This uses bottom up dynamic programming by starting at the bottom, we ensure that every decision made at a higher row
8+
is based on the perfect knowledge of the best possible paths below it
9+
10+
Complexity:
11+
Time Complexity results in O(n^2), since we visit every number in the triangle exactly once. For n rows, there are
12+
roughtl n^2/2 elements.
13+
Space Complexity is O(1) since the triangle input array is updated in place
14+
15+
Args:
16+
triangle(list): A list of lists of integers representing the triangle
17+
Returns:
18+
The minimum path sum in the triangle
19+
"""
20+
row_count = len(triangle)
21+
22+
# start from the second to last row, since the bottom row has no children to begin with
23+
for row in range(row_count - 2, -1, -1):
24+
# ensures that we visit every element for the current row
25+
for col in range(row + 1):
26+
# Each cell is updated to include the minimum path sum from the row below
27+
triangle[row][col] += min(
28+
triangle[row + 1][col], triangle[row + 1][col + 1]
29+
)
30+
31+
# the result trickles to the apex
32+
return triangle[0][0]
33+
34+
35+
def min_path_sum_2(triangle: List[List[int]]) -> int:
36+
"""
37+
Finds the minimum path sum in a given triangle of numbers
38+
This uses bottom up dynamic programming by starting at the bottom, we ensure that every decision made at a higher row
39+
is based on the perfect knowledge of the best possible paths below it
40+
41+
Complexity:
42+
Time Complexity results in O(n^2), since we visit every number in the triangle exactly once. For n rows, there are
43+
roughtl n^2/2 elements.
44+
Space Complexity is O(n) since the triangle input array's last row is copied over
45+
46+
Args:
47+
triangle(list): A list of lists of integers representing the triangle
48+
Returns:
49+
The minimum path sum in the triangle
50+
"""
51+
dp = triangle[-1][:]
52+
row_count = len(triangle)
53+
54+
# start from the second to last row, since the bottom row has no children to begin with
55+
for row_idx in range(row_count - 2, -1, -1):
56+
# ensures that we visit every element for the current row
57+
for col_idx in range(row_idx + 1):
58+
# Each cell is updated to include the minimum path sum from the row below
59+
dp[col_idx] = triangle[row_idx][col_idx] + min(dp[col_idx], dp[col_idx + 1])
60+
61+
# the result trickles to the apex
62+
return dp[0]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import unittest
2+
import copy
3+
from typing import List
4+
from parameterized import parameterized
5+
from algorithms.dynamic_programming.min_path_sum import min_path_sum, min_path_sum_2
6+
7+
TEST_CASES = [
8+
([[5]], 5),
9+
([[2], [3, 4]], 5),
10+
([[1000], [2000, 3000]], 3000),
11+
([[2], [3, 4], [6, 5, 7], [4, 1, 8, 3]], 11),
12+
([[7], [3, 8], [8, 1, 0], [2, 7, 4, 4], [4, 5, 2, 6, 5]], 17),
13+
]
14+
15+
16+
class MinPathSumInTriangleTestCase(unittest.TestCase):
17+
@parameterized.expand(TEST_CASES)
18+
def test_min_path_sum_in_triangle(self, triangle: List[List[int]], expected: int):
19+
input_triangle = copy.deepcopy(triangle)
20+
actual = min_path_sum(input_triangle)
21+
self.assertEqual(expected, actual)
22+
23+
@parameterized.expand(TEST_CASES)
24+
def test_min_path_sum_2_in_triangle(self, triangle: List[List[int]], expected: int):
25+
input_triangle = copy.deepcopy(triangle)
26+
actual = min_path_sum_2(input_triangle)
27+
self.assertEqual(expected, actual)
28+
29+
30+
if __name__ == "__main__":
31+
unittest.main()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import Optional
2+
from datastructures.linked_lists import Node
3+
4+
5+
def find_middle_node(head: Optional[Node]) -> Optional[Node]:
6+
"""
7+
Traverse the linked list to find the middle node
8+
Time Complexity: O(n) where n is the number of nodes in the linked list
9+
Space Complexity: O(1) as constant extra space is needed
10+
@return: Middle Node or None
11+
"""
12+
if not head:
13+
return None
14+
15+
fast_pointer, slow_pointer = head, head
16+
17+
while fast_pointer and fast_pointer.next:
18+
slow_pointer = slow_pointer.next
19+
fast_pointer = fast_pointer.next.next
20+
21+
return slow_pointer

0 commit comments

Comments
 (0)