Skip to content

Commit dc19fc4

Browse files
authored
Merge pull request #180 from BrianLusina/feat/algorithms-graphs
feat(algorithms, graphs): min cost to make at least one valid path in a grid
2 parents 09c95f1 + 92b9ed0 commit dc19fc4

18 files changed

+697
-0
lines changed

DIRECTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@
143143
* [Test Max Area Of Island](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/maxareaofisland/test_max_area_of_island.py)
144144
* Min Cost To Supply
145145
* [Test Min Cost To Supply](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/min_cost_to_supply/test_min_cost_to_supply.py)
146+
* Min Cost Valid Path
147+
* [Test Min Cost To Make Valid Path](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/min_cost_valid_path/test_min_cost_to_make_valid_path.py)
146148
* Nearest Exit From Entrance In Maze
147149
* [Test Nearest Exit From Entrance](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/nearest_exit_from_entrance_in_maze/test_nearest_exit_from_entrance.py)
148150
* Network Delay Time

algorithms/graphs/min_cost_valid_path/README.md

Lines changed: 385 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
from typing import List
2+
import heapq
3+
from collections import deque
4+
import sys
5+
6+
7+
def min_cost_dp(grid: List[List[int]]) -> int:
8+
if not grid:
9+
return 0
10+
11+
num_rows = len(grid)
12+
num_cols = len(grid[0])
13+
14+
min_changes = [[float("inf")] * num_cols for _ in range(num_rows)]
15+
min_changes[0][0] = 0
16+
17+
while True:
18+
# Store previous state to check for convergence
19+
prev_state = [row[:] for row in min_changes]
20+
21+
# forward pass: check cells coming from left and top
22+
for row in range(num_rows):
23+
for col in range(num_cols):
24+
# check cell above
25+
if row > 0:
26+
min_changes[row][col] = min(
27+
min_changes[row][col],
28+
min_changes[row - 1][col]
29+
+ (0 if grid[row - 1][col] == 3 else 1),
30+
)
31+
32+
# check cell to the left
33+
if col > 0:
34+
min_changes[row][col] = min(
35+
min_changes[row][col],
36+
min_changes[row][col - 1]
37+
+ (0 if grid[row][col - 1] == 1 else 1),
38+
)
39+
40+
# backward pass: check cells coming from right and bottom
41+
for row in range(num_rows - 1, -1, -1):
42+
for col in range(num_cols - 1, -1, -1):
43+
# check cell below
44+
if row < num_rows - 1:
45+
min_changes[row][col] = min(
46+
min_changes[row][col],
47+
min_changes[row + 1][col]
48+
+ (0 if grid[row + 1][col] == 4 else 1),
49+
)
50+
51+
# Check cell to the right
52+
if col < num_cols - 1:
53+
min_changes[row][col] = min(
54+
min_changes[row][col],
55+
min_changes[row][col + 1]
56+
+ (0 if grid[row][col + 1] == 2 else 1),
57+
)
58+
59+
# if not changes were made in this operation, we've found optimal solution
60+
if min_changes == prev_state:
61+
break
62+
63+
return min_changes[num_rows - 1][num_cols - 1]
64+
65+
66+
def min_cost_dijkstra(grid: List[List[int]]) -> int:
67+
if not grid:
68+
return 0
69+
70+
dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
71+
num_rows = len(grid)
72+
num_cols = len(grid[0])
73+
74+
# Min-heap ordered by cost. Each element is (cost, row, col)
75+
# Using list as heap, elements are tuples
76+
pq = [(0, 0, 0)]
77+
78+
cost_grid = [[float("inf")] * num_cols for _ in range(num_rows)]
79+
cost_grid[0][0] = 0
80+
81+
while pq:
82+
cost, row, col = heapq.heappop(pq)
83+
84+
# skip if we've found a better path to this cell
85+
if cost_grid[row][col] != cost:
86+
continue
87+
88+
# Try all 4 directions
89+
for d, (dr, dc) in enumerate(dirs):
90+
new_row = row + dr
91+
new_col = col + dc
92+
93+
# Check if new position is valid
94+
if 0 <= new_row < num_rows and 0 <= new_col < num_cols:
95+
# add cost = 1 if we need to change direction
96+
new_cost = cost + (d != (grid[row][col] - 1))
97+
98+
# update if we found a better path
99+
if cost_grid[new_row][new_col] > new_cost:
100+
cost_grid[new_row][new_col] = new_cost
101+
heapq.heappush(pq, (new_cost, new_row, new_col))
102+
103+
return cost_grid[num_rows - 1][num_cols - 1]
104+
105+
106+
def min_cost_0_1_bfs(grid: List[List[int]]) -> int:
107+
if not grid:
108+
return 0
109+
num_rows = len(grid)
110+
num_cols = len(grid[0])
111+
# Direction vectors: right, left, down, up (matching grid values 1,2,3,4)
112+
dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
113+
114+
cost_grid = [[float("inf")] * num_cols for _ in range(num_rows)]
115+
cost_grid[0][0] = 0
116+
117+
# Use deque for 0-1 BFS - add zero cost moves to front, cost=1 to back
118+
queue = deque([(0, 0)])
119+
120+
# Check if coordinates are within grid bounds
121+
def is_valid(row: int, col: int) -> bool:
122+
return 0 <= row < num_rows and 0 <= col < num_cols
123+
124+
while queue:
125+
row, col = queue.popleft()
126+
# Try all four directions
127+
for dir_idx, (dx, dy) in enumerate(dirs):
128+
new_row, new_col = row + dx, col + dy
129+
cost = 0 if grid[row][col] == dir_idx + 1 else 1
130+
131+
# If position is valid and we found a better path
132+
if (
133+
is_valid(new_row, new_col)
134+
and cost_grid[row][col] + cost < cost_grid[new_row][new_col]
135+
):
136+
cost_grid[new_row][new_col] = cost_grid[row][col] + cost
137+
138+
# Add to back if cost=1, front if cost=0
139+
if cost == 1:
140+
queue.append((new_row, new_col))
141+
else:
142+
queue.appendleft((new_row, new_col))
143+
144+
return cost_grid[num_rows - 1][num_cols - 1]
145+
146+
147+
def min_cost_0_1_bfs_2(grid: List[List[int]]) -> int:
148+
if not grid:
149+
return 0
150+
# Store the number of rows and columns of grid
151+
num_rows, num_cols = len(grid), len(grid[0])
152+
153+
# Create a 2D array of size num_rows x num_cols, initializing all cells to the maximum integer value
154+
cost_grid = [[sys.maxsize] * num_cols for _ in range(num_rows)]
155+
156+
# Helper function to check if the new cell is valid and its cost can be improved
157+
def is_valid_and_improvable(row, col) -> bool:
158+
return (
159+
0 <= row < len(cost_grid)
160+
and 0 <= col < len(cost_grid[0])
161+
and cost_grid[row][col] != 0
162+
)
163+
164+
# Create a deque and push the starting cell (0, 0) to the front
165+
dq = deque()
166+
dq.appendleft((0, 0))
167+
168+
# Set its cost in cost_grid to 0
169+
cost_grid[0][0] = 0
170+
171+
# Define an array representing the four possible movement directions
172+
dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]
173+
174+
# Enter a loop that continues as long as the deque is not empty
175+
while dq:
176+
# Pop the front cell from the deque and store its coordinates in row and col
177+
row, col = dq.popleft()
178+
179+
# Loop through each of the four directions in dirs
180+
for d in range(4):
181+
# Compute the coordinates of the adjacent cell
182+
new_row = row + dirs[d][0]
183+
new_col = col + dirs[d][1]
184+
185+
# Check if the new cell is valid and its cost can be improved
186+
if is_valid_and_improvable(new_row, new_col):
187+
# Calculate the movement cost
188+
cost = 1 if grid[row][col] != (d + 1) else 0
189+
190+
# Check whether the new cost is less than the current cost at the adjacent cell
191+
if cost_grid[row][col] + cost < cost_grid[new_row][new_col]:
192+
# Update the cost of the adjacent cell
193+
cost_grid[new_row][new_col] = cost_grid[row][col] + cost
194+
195+
if cost == 1:
196+
# Push the new cell to the back
197+
dq.append((new_row, new_col))
198+
else:
199+
# Push the new cell to the front
200+
dq.appendleft((new_row, new_col))
201+
202+
# Return the minimum cost stored at the bottom-right cell
203+
return cost_grid[num_rows - 1][num_cols - 1]
204+
205+
206+
def min_cost_dfs_and_bfs(grid: List[List[int]]) -> int:
207+
if not grid:
208+
return 0
209+
# Direction vectors: right, left, down, up (matching grid values 1,2,3,4)
210+
dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
211+
num_rows = len(grid)
212+
num_cols = len(grid[0])
213+
cost = 0
214+
215+
# Track minimum cost to reach each cell
216+
cost_grid = [[float("inf")] * num_cols for _ in range(num_rows)]
217+
218+
queue = deque()
219+
220+
# DFS to explore all reachable cells with current cost
221+
def dfs(
222+
row: int,
223+
col: int,
224+
cost: int,
225+
) -> None:
226+
if not is_unvisited(row, col):
227+
return
228+
229+
cost_grid[row][col] = cost
230+
queue.append((row, col))
231+
232+
# Follow the arrow direction without cost increase
233+
next_dir = grid[row][col] - 1
234+
dx, dy = dirs[next_dir]
235+
dfs(row + dx, col + dy, cost)
236+
237+
# Check if cell is within bounds and unvisited
238+
def is_unvisited(row: int, col: int) -> bool:
239+
return (
240+
0 <= row < len(cost_grid)
241+
and 0 <= col < len(cost_grid[0])
242+
and cost_grid[row][col] == float("inf")
243+
)
244+
245+
dfs(0, 0, cost)
246+
247+
# BFS part - process cells level by level with increasing cost
248+
while queue:
249+
cost += 1
250+
level_size = len(queue)
251+
252+
for _ in range(level_size):
253+
row, col = queue.popleft()
254+
255+
# Try all 4 directions for next level
256+
for dir_idx, (dx, dy) in enumerate(dirs):
257+
dfs(row + dx, col + dy, cost)
258+
259+
return cost_grid[num_rows - 1][num_cols - 1]
29.5 KB
Loading
44.6 KB
Loading
25 KB
Loading
18.8 KB
Loading
15.2 KB
Loading
7.56 KB
Loading
61.2 KB
Loading

0 commit comments

Comments
 (0)