From 568cc9b901a420cac8eaf3db1c03e8fbf9cfbf8e Mon Sep 17 00:00:00 2001 From: Lusina <12752833+BrianLusina@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:49:26 +0300 Subject: [PATCH] feat(algorithms, dynamic-programming): unique paths --- .../unique_paths/__init__.py | 41 ++++++----- .../unique_paths/test_unique_paths.py | 72 +++++++------------ 2 files changed, 47 insertions(+), 66 deletions(-) diff --git a/algorithms/dynamic_programming/unique_paths/__init__.py b/algorithms/dynamic_programming/unique_paths/__init__.py index 91c9fd6f..13a3651f 100644 --- a/algorithms/dynamic_programming/unique_paths/__init__.py +++ b/algorithms/dynamic_programming/unique_paths/__init__.py @@ -1,5 +1,5 @@ from typing import Dict, Tuple - +from functools import lru_cache def unique_paths_math(m: int, n: int) -> int: """Uses math formula""" @@ -28,6 +28,7 @@ def unique_paths_top_down(m: int, n: int) -> int: """ cache: Dict[Tuple[int, int], int] = {} + @lru_cache(None) def unique_path_helper(row: int, col: int) -> int: # If already calculated, re-use those if (row, col) in cache: @@ -48,28 +49,30 @@ def unique_path_helper(row: int, col: int) -> int: return unique_path_helper(m, n) -def unique_paths_bottom_up(m: int, n: int) -> int: +def unique_paths_bottom_up(rows: int, cols: int) -> int: """Uses bottom-up approach - Complexity Analysis: - - Time Complexity: O(m*n) - - Space Complexity: O(n) + Complexity Analysis + + Time Complexity: O(m * n) where m and n are the dimensions of the grid. For both the recursive and iterative + solutions, we need to fill in a table of size m x n. + + Space Complexity: O(m * n) where m and n are the dimensions of the grid. The recursive solution uses O(m * n) space + due to the call stack, while the iterative solution uses O(m * n) space for the dp table. """ - row = [1] * n + dp = [[0] * cols for _ in range(rows)] - # go through all rows except the last one - for i in range(m - 1): - new_row = [1] * n + # Set base case: there is only one way to reach any cell in the first row (moving only right) + for r in range(cols): + dp[0][r] = 1 - # go through every column except the right most column - # because the last value in every row is 1 - # start at second to last position and - # keep going until we get to the beginning (reverse order) + # Set base case: there is only one way to reach any cell in the first column (moving only down) + for r in range(rows): + dp[r][0] = 1 - for j in range(n - 2, -1, -1): - # right value + value below - new_row[j] = new_row[j + 1] + row[j] - # update the row - row = new_row + # Fill in the rest of the table + for i in range(1, rows): + for j in range(1, cols): + dp[i][j] = dp[i-1][j] + dp[i][j-1] - return row[0] + return dp[rows-1][cols-1] diff --git a/algorithms/dynamic_programming/unique_paths/test_unique_paths.py b/algorithms/dynamic_programming/unique_paths/test_unique_paths.py index cae4b15e..9ddf3ae9 100644 --- a/algorithms/dynamic_programming/unique_paths/test_unique_paths.py +++ b/algorithms/dynamic_programming/unique_paths/test_unique_paths.py @@ -1,57 +1,35 @@ import unittest -from . import unique_paths_top_down, unique_paths_math, unique_paths_bottom_up - - -class UniquePathsTopDownTestCase(unittest.TestCase): - def test_1(self): - """should return 28 for m=3 and n=7""" - m = 3 - n = 7 - expected = 28 +from parameterized import parameterized +from algorithms.dynamic_programming.unique_paths import ( + unique_paths_top_down, + unique_paths_math, + unique_paths_bottom_up, +) + +UNIQUE_PATHS_TEST_CASES = [ + (3, 7, 28), + (3, 2, 3), + (2, 2, 2), + (3, 3, 6), + (1, 1, 1), + (10, 10, 48620), + (20, 20, 35345263800), +] + + +class UniquePathsTestCase(unittest.TestCase): + @parameterized.expand(UNIQUE_PATHS_TEST_CASES) + def test_unique_paths_top_down(self, m: int, n: int, expected: int): actual = unique_paths_top_down(m, n) self.assertEqual(expected, actual) - def test_2(self): - """should return 3 for m=3 and n=2""" - m = 3 - n = 2 - expected = 3 - actual = unique_paths_top_down(m, n) - self.assertEqual(expected, actual) - - -class UniquePathsBottomUpTestCase(unittest.TestCase): - def test_1(self): - """should return 28 for m=3 and n=7""" - m = 3 - n = 7 - expected = 28 + @parameterized.expand(UNIQUE_PATHS_TEST_CASES) + def test_unique_paths_bottom_up(self, m: int, n: int, expected: int): actual = unique_paths_bottom_up(m, n) self.assertEqual(expected, actual) - def test_2(self): - """should return 3 for m=3 and n=2""" - m = 3 - n = 2 - expected = 3 - actual = unique_paths_bottom_up(m, n) - self.assertEqual(expected, actual) - - -class UniquePathsMathTestCase(unittest.TestCase): - def test_1(self): - """should return 28 for m=3 and n=7""" - m = 3 - n = 7 - expected = 28 - actual = unique_paths_math(m, n) - self.assertEqual(expected, actual) - - def test_2(self): - """should return 3 for m=3 and n=2""" - m = 3 - n = 2 - expected = 3 + @parameterized.expand(UNIQUE_PATHS_TEST_CASES) + def test_unique_paths_math(self, m: int, n: int, expected: int): actual = unique_paths_math(m, n) self.assertEqual(expected, actual)