-
Notifications
You must be signed in to change notification settings - Fork 2
feat(algorithms, graphs): min cost to make at least one valid path in a grid #180
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
Show all changes
6 commits
Select commit
Hold shift + click to select a range
15f0672
feat(algorithms, graphs): min cost to make at least one valid path in…
BrianLusina f09ca8e
updating DIRECTORY.md
963b8f3
docs(algorithms, graphs): add solution heading
BrianLusina 329fc6c
Update algorithms/graphs/min_cost_valid_path/__init__.py
BrianLusina 5916082
Update algorithms/graphs/min_cost_valid_path/README.md
BrianLusina 92b9ed0
refactor(algorithms, graphs): empty grid check
BrianLusina 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
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
Large diffs are not rendered by default.
Oops, something went wrong.
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,259 @@ | ||
| from typing import List | ||
| import heapq | ||
| from collections import deque | ||
| import sys | ||
|
|
||
|
|
||
| def min_cost_dp(grid: List[List[int]]) -> int: | ||
| if not grid: | ||
| return 0 | ||
|
|
||
| num_rows = len(grid) | ||
| num_cols = len(grid[0]) | ||
|
|
||
| min_changes = [[float("inf")] * num_cols for _ in range(num_rows)] | ||
| min_changes[0][0] = 0 | ||
|
|
||
| while True: | ||
| # Store previous state to check for convergence | ||
| prev_state = [row[:] for row in min_changes] | ||
|
|
||
| # forward pass: check cells coming from left and top | ||
| for row in range(num_rows): | ||
| for col in range(num_cols): | ||
| # check cell above | ||
| if row > 0: | ||
| min_changes[row][col] = min( | ||
| min_changes[row][col], | ||
| min_changes[row - 1][col] | ||
| + (0 if grid[row - 1][col] == 3 else 1), | ||
| ) | ||
|
|
||
| # check cell to the left | ||
| if col > 0: | ||
| min_changes[row][col] = min( | ||
| min_changes[row][col], | ||
| min_changes[row][col - 1] | ||
| + (0 if grid[row][col - 1] == 1 else 1), | ||
| ) | ||
|
|
||
| # backward pass: check cells coming from right and bottom | ||
| for row in range(num_rows - 1, -1, -1): | ||
| for col in range(num_cols - 1, -1, -1): | ||
| # check cell below | ||
| if row < num_rows - 1: | ||
| min_changes[row][col] = min( | ||
| min_changes[row][col], | ||
| min_changes[row + 1][col] | ||
| + (0 if grid[row + 1][col] == 4 else 1), | ||
| ) | ||
|
|
||
| # Check cell to the right | ||
| if col < num_cols - 1: | ||
| min_changes[row][col] = min( | ||
| min_changes[row][col], | ||
| min_changes[row][col + 1] | ||
| + (0 if grid[row][col + 1] == 2 else 1), | ||
| ) | ||
|
|
||
| # if not changes were made in this operation, we've found optimal solution | ||
| if min_changes == prev_state: | ||
| break | ||
|
|
||
| return min_changes[num_rows - 1][num_cols - 1] | ||
|
|
||
|
|
||
| def min_cost_dijkstra(grid: List[List[int]]) -> int: | ||
| if not grid: | ||
| return 0 | ||
|
|
||
| dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)] | ||
| num_rows = len(grid) | ||
| num_cols = len(grid[0]) | ||
|
|
||
| # Min-heap ordered by cost. Each element is (cost, row, col) | ||
| # Using list as heap, elements are tuples | ||
| pq = [(0, 0, 0)] | ||
|
|
||
| cost_grid = [[float("inf")] * num_cols for _ in range(num_rows)] | ||
| cost_grid[0][0] = 0 | ||
|
|
||
| while pq: | ||
| cost, row, col = heapq.heappop(pq) | ||
|
|
||
| # skip if we've found a better path to this cell | ||
| if cost_grid[row][col] != cost: | ||
| continue | ||
|
|
||
| # Try all 4 directions | ||
| for d, (dr, dc) in enumerate(dirs): | ||
| new_row = row + dr | ||
| new_col = col + dc | ||
|
|
||
| # Check if new position is valid | ||
| if 0 <= new_row < num_rows and 0 <= new_col < num_cols: | ||
| # add cost = 1 if we need to change direction | ||
| new_cost = cost + (d != (grid[row][col] - 1)) | ||
|
|
||
| # update if we found a better path | ||
| if cost_grid[new_row][new_col] > new_cost: | ||
| cost_grid[new_row][new_col] = new_cost | ||
| heapq.heappush(pq, (new_cost, new_row, new_col)) | ||
|
|
||
| return cost_grid[num_rows - 1][num_cols - 1] | ||
|
|
||
|
|
||
| def min_cost_0_1_bfs(grid: List[List[int]]) -> int: | ||
| if not grid: | ||
| return 0 | ||
| num_rows = len(grid) | ||
| num_cols = len(grid[0]) | ||
| # Direction vectors: right, left, down, up (matching grid values 1,2,3,4) | ||
| dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)] | ||
|
|
||
| cost_grid = [[float("inf")] * num_cols for _ in range(num_rows)] | ||
| cost_grid[0][0] = 0 | ||
|
|
||
| # Use deque for 0-1 BFS - add zero cost moves to front, cost=1 to back | ||
| queue = deque([(0, 0)]) | ||
|
|
||
| # Check if coordinates are within grid bounds | ||
| def is_valid(row: int, col: int) -> bool: | ||
| return 0 <= row < num_rows and 0 <= col < num_cols | ||
|
|
||
| while queue: | ||
| row, col = queue.popleft() | ||
| # Try all four directions | ||
| for dir_idx, (dx, dy) in enumerate(dirs): | ||
| new_row, new_col = row + dx, col + dy | ||
| cost = 0 if grid[row][col] == dir_idx + 1 else 1 | ||
|
|
||
| # If position is valid and we found a better path | ||
| if ( | ||
| is_valid(new_row, new_col) | ||
| and cost_grid[row][col] + cost < cost_grid[new_row][new_col] | ||
| ): | ||
| cost_grid[new_row][new_col] = cost_grid[row][col] + cost | ||
|
|
||
| # Add to back if cost=1, front if cost=0 | ||
| if cost == 1: | ||
| queue.append((new_row, new_col)) | ||
| else: | ||
| queue.appendleft((new_row, new_col)) | ||
|
|
||
| return cost_grid[num_rows - 1][num_cols - 1] | ||
|
|
||
|
|
||
| def min_cost_0_1_bfs_2(grid: List[List[int]]) -> int: | ||
| if not grid: | ||
| return 0 | ||
| # Store the number of rows and columns of grid | ||
| num_rows, num_cols = len(grid), len(grid[0]) | ||
|
|
||
| # Create a 2D array of size num_rows x num_cols, initializing all cells to the maximum integer value | ||
| cost_grid = [[sys.maxsize] * num_cols for _ in range(num_rows)] | ||
|
|
||
| # Helper function to check if the new cell is valid and its cost can be improved | ||
| def is_valid_and_improvable(row, col) -> bool: | ||
| return ( | ||
| 0 <= row < len(cost_grid) | ||
| and 0 <= col < len(cost_grid[0]) | ||
| and cost_grid[row][col] != 0 | ||
| ) | ||
|
|
||
| # Create a deque and push the starting cell (0, 0) to the front | ||
| dq = deque() | ||
| dq.appendleft((0, 0)) | ||
|
|
||
| # Set its cost in cost_grid to 0 | ||
| cost_grid[0][0] = 0 | ||
|
|
||
| # Define an array representing the four possible movement directions | ||
| dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]] | ||
|
|
||
| # Enter a loop that continues as long as the deque is not empty | ||
| while dq: | ||
| # Pop the front cell from the deque and store its coordinates in row and col | ||
| row, col = dq.popleft() | ||
|
|
||
| # Loop through each of the four directions in dirs | ||
| for d in range(4): | ||
| # Compute the coordinates of the adjacent cell | ||
| new_row = row + dirs[d][0] | ||
| new_col = col + dirs[d][1] | ||
|
|
||
| # Check if the new cell is valid and its cost can be improved | ||
| if is_valid_and_improvable(new_row, new_col): | ||
| # Calculate the movement cost | ||
| cost = 1 if grid[row][col] != (d + 1) else 0 | ||
|
|
||
| # Check whether the new cost is less than the current cost at the adjacent cell | ||
| if cost_grid[row][col] + cost < cost_grid[new_row][new_col]: | ||
| # Update the cost of the adjacent cell | ||
| cost_grid[new_row][new_col] = cost_grid[row][col] + cost | ||
|
|
||
| if cost == 1: | ||
| # Push the new cell to the back | ||
| dq.append((new_row, new_col)) | ||
| else: | ||
| # Push the new cell to the front | ||
| dq.appendleft((new_row, new_col)) | ||
|
|
||
| # Return the minimum cost stored at the bottom-right cell | ||
| return cost_grid[num_rows - 1][num_cols - 1] | ||
|
|
||
|
|
||
| def min_cost_dfs_and_bfs(grid: List[List[int]]) -> int: | ||
| if not grid: | ||
| return 0 | ||
| # Direction vectors: right, left, down, up (matching grid values 1,2,3,4) | ||
| dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)] | ||
| num_rows = len(grid) | ||
| num_cols = len(grid[0]) | ||
| cost = 0 | ||
|
|
||
| # Track minimum cost to reach each cell | ||
| cost_grid = [[float("inf")] * num_cols for _ in range(num_rows)] | ||
|
|
||
| queue = deque() | ||
|
|
||
| # DFS to explore all reachable cells with current cost | ||
| def dfs( | ||
| row: int, | ||
| col: int, | ||
| cost: int, | ||
| ) -> None: | ||
| if not is_unvisited(row, col): | ||
| return | ||
|
|
||
| cost_grid[row][col] = cost | ||
| queue.append((row, col)) | ||
|
|
||
| # Follow the arrow direction without cost increase | ||
| next_dir = grid[row][col] - 1 | ||
| dx, dy = dirs[next_dir] | ||
| dfs(row + dx, col + dy, cost) | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # Check if cell is within bounds and unvisited | ||
| def is_unvisited(row: int, col: int) -> bool: | ||
| return ( | ||
| 0 <= row < len(cost_grid) | ||
| and 0 <= col < len(cost_grid[0]) | ||
| and cost_grid[row][col] == float("inf") | ||
| ) | ||
|
|
||
| dfs(0, 0, cost) | ||
|
|
||
| # BFS part - process cells level by level with increasing cost | ||
| while queue: | ||
| cost += 1 | ||
| level_size = len(queue) | ||
|
|
||
| for _ in range(level_size): | ||
| row, col = queue.popleft() | ||
|
|
||
| # Try all 4 directions for next level | ||
| for dir_idx, (dx, dy) in enumerate(dirs): | ||
| dfs(row + dx, col + dy, cost) | ||
|
|
||
| return cost_grid[num_rows - 1][num_cols - 1] | ||
Binary file added
BIN
+29.5 KB
...st_valid_path/images/examples/min_cost_to_make_valid_path_in_grid_example_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+44.6 KB
...st_valid_path/images/examples/min_cost_to_make_valid_path_in_grid_example_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+25 KB
...st_valid_path/images/examples/min_cost_to_make_valid_path_in_grid_example_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+18.8 KB
...st_valid_path/images/examples/min_cost_to_make_valid_path_in_grid_example_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+15.2 KB
...st_valid_path/images/examples/min_cost_to_make_valid_path_in_grid_example_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+7.56 KB
...st_valid_path/images/examples/min_cost_to_make_valid_path_in_grid_example_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+61.2 KB
..._valid_path/images/solutions/min_cost_to_make_valid_path_in_grid_solution_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+62.2 KB
..._valid_path/images/solutions/min_cost_to_make_valid_path_in_grid_solution_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+62.6 KB
..._valid_path/images/solutions/min_cost_to_make_valid_path_in_grid_solution_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+61.8 KB
..._valid_path/images/solutions/min_cost_to_make_valid_path_in_grid_solution_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+61.1 KB
..._valid_path/images/solutions/min_cost_to_make_valid_path_in_grid_solution_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+60.3 KB
..._valid_path/images/solutions/min_cost_to_make_valid_path_in_grid_solution_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+59.4 KB
..._valid_path/images/solutions/min_cost_to_make_valid_path_in_grid_solution_7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+60.8 KB
..._valid_path/images/solutions/min_cost_to_make_valid_path_in_grid_solution_8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions
51
algorithms/graphs/min_cost_valid_path/test_min_cost_to_make_valid_path.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,51 @@ | ||
| import unittest | ||
| from typing import List | ||
| from parameterized import parameterized | ||
| from algorithms.graphs.min_cost_valid_path import ( | ||
| min_cost_dp, | ||
| min_cost_dijkstra, | ||
| min_cost_0_1_bfs, | ||
| min_cost_0_1_bfs_2, | ||
| min_cost_dfs_and_bfs | ||
| ) | ||
|
|
||
| MIN_COST_TO_MAKE_VALID_PATH_IN_GRID_TEST_CASES = [ | ||
| ([[1, 1, 1, 1], [2, 2, 2, 2], [1, 1, 1, 1], [2, 2, 2, 2]], 3), | ||
| ([[1, 1, 3], [3, 2, 2], [1, 1, 4]], 0), | ||
| ([[1, 2], [4, 3]], 1), | ||
| ([[4]], 0), | ||
| ([[1, 1], [1, 1]], 1), | ||
| ([[4, 3, 4, 3], [3, 4, 3, 4]], 3), | ||
| ([[1, 1, 3], [2, 2, 3], [1, 1, 4]], 0), | ||
| ] | ||
|
|
||
|
|
||
| class MinCostToMakeValidPathTestCase(unittest.TestCase): | ||
| @parameterized.expand(MIN_COST_TO_MAKE_VALID_PATH_IN_GRID_TEST_CASES) | ||
| def test_min_cost(self, grid: List[List[int]], expected: int): | ||
| actual = min_cost_dp(grid) | ||
| self.assertEqual(expected, actual) | ||
|
|
||
| @parameterized.expand(MIN_COST_TO_MAKE_VALID_PATH_IN_GRID_TEST_CASES) | ||
| def test_min_cost_dijkstra(self, grid: List[List[int]], expected: int): | ||
| actual = min_cost_dijkstra(grid) | ||
| self.assertEqual(expected, actual) | ||
|
|
||
| @parameterized.expand(MIN_COST_TO_MAKE_VALID_PATH_IN_GRID_TEST_CASES) | ||
| def test_min_cost_0_1_bfs(self, grid: List[List[int]], expected: int): | ||
| actual = min_cost_0_1_bfs(grid) | ||
| self.assertEqual(expected, actual) | ||
|
|
||
| @parameterized.expand(MIN_COST_TO_MAKE_VALID_PATH_IN_GRID_TEST_CASES) | ||
| def test_min_cost_0_1_bfs_2(self, grid: List[List[int]], expected: int): | ||
| actual = min_cost_0_1_bfs_2(grid) | ||
| self.assertEqual(expected, actual) | ||
|
|
||
| @parameterized.expand(MIN_COST_TO_MAKE_VALID_PATH_IN_GRID_TEST_CASES) | ||
| def test_min_cost_dfs_and_bfs(self, grid: List[List[int]], expected: int): | ||
| actual = min_cost_dfs_and_bfs(grid) | ||
| self.assertEqual(expected, actual) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
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.