Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
55 changes: 55 additions & 0 deletions algorithms/dynamic_programming/min_path_sum/README.md
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.
62 changes: 62 additions & 0 deletions algorithms/dynamic_programming/min_path_sum/__init__.py
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]
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()
21 changes: 21 additions & 0 deletions datastructures/linked_lists/linked_list_utils.py
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
Loading