1575. Count All Possible Routes
Problem Description
You have an array locations
containing distinct positive integers, where locations[i]
represents the position of city i
. You're given three additional parameters:
start
: the index of your starting cityfinish
: the index of your destination cityfuel
: the initial amount of fuel you have
Movement Rules:
- From any city
i
, you can move to any other cityj
(wherej ≠ i
) - Moving from city
i
to cityj
costs|locations[i] - locations[j]|
fuel (the absolute difference between their positions) - You cannot make a move if it would cause your fuel to become negative
- You can visit any city multiple times, including the start and finish cities
Goal: Count all possible routes from the start
city to the finish
city without running out of fuel.
Important Notes:
- A route is considered valid as long as it ends at the
finish
city with non-negative fuel remaining - You get credit for reaching the finish city each time you arrive there - if you're already at the finish city, that counts as one valid route, and you can continue traveling to accumulate more routes
- Return the total count modulo
10^9 + 7
since the answer might be very large
Example Understanding:
If you start at city start
and it happens to be the same as finish
, that immediately counts as one valid route. You can then travel to other cities and return to finish
again, with each arrival counting as another valid route, as long as you have sufficient fuel for each journey.
Intuition
The key insight is that this is a path counting problem where we need to explore all possible routes. Since we can revisit cities multiple times, we might end up calculating the same subproblem repeatedly - for instance, arriving at the same city with the same amount of fuel through different routes.
Think about it this way: if we're at city i
with k
fuel remaining, the number of valid routes from this state to the finish doesn't depend on how we got to city i
- it only depends on our current position and remaining fuel. This observation suggests using dynamic programming with memoization.
We can break down the problem recursively:
- From any city
i
withk
fuel, we need to count all possible ways to reach the finish - We can try moving to every other city
j
, which costs|locations[i] - locations[j]|
fuel - For each valid move (where we have enough fuel), we recursively count the routes from the new position
The base cases emerge naturally:
- If we're at the finish city, we've found one valid route (but we can continue traveling to find more)
- If we don't have enough fuel to reach the finish city even by the shortest possible path (
k < |locations[i] - locations[finish]|
), then there are no valid routes from this state
The beauty of this approach is that by using memoization with @cache
, we avoid recalculating the number of routes from the same (city, fuel)
state multiple times. Each unique state is computed only once, making the solution efficient despite the potentially large number of possible paths.
The recursive function dfs(i, k)
naturally captures this idea: "How many ways can I reach the finish from city i
with k
fuel?" The answer accumulates by trying all possible next moves and summing up their route counts.
Learn more about Memoization and Dynamic Programming patterns.
Solution Approach
The solution uses memoization with depth-first search (DFS) to count all possible routes efficiently.
Core Function Design:
We define dfs(i, k)
to represent the number of valid routes from city i
to the finish city with k
units of fuel remaining. The answer to our problem is simply dfs(start, fuel)
.
Implementation Steps:
-
Early Termination Check:
- First, we check if
k < |locations[i] - locations[finish]|
- This means we don't have enough fuel to reach the finish even by the most direct route
- If true, return
0
as no valid routes exist from this state
- First, we check if
-
Initialize Route Count:
- Set
ans = 1
ifi == finish
(we're already at the destination), otherwiseans = 0
- This accounts for the current position being a valid route if it's the finish
- Set
-
Explore All Possible Moves:
- Iterate through all cities
j
usingenumerate(locations)
- For each city
j
wherej != i
:- Calculate fuel cost:
|locations[i] - locations[j]|
- Recursively call
dfs(j, k - fuel_cost)
to get routes from the new position - Add the result to
ans
(with modulo operation to prevent overflow)
- Calculate fuel cost:
- Iterate through all cities
-
Memoization with
@cache
:- The
@cache
decorator automatically stores results ofdfs(i, k)
calls - When the same
(i, k)
state is encountered again, the cached result is returned immediately - This prevents redundant calculations and reduces time complexity significantly
- The
Why This Works:
- The recursive structure naturally explores all possible paths
- Each valid route that reaches the finish contributes
1
to the total count - By summing contributions from all possible next moves, we count every unique route exactly once
- The memoization ensures we compute each unique state
(city, fuel)
only once, even though many different paths might lead to the same state
Time Complexity: O(n² × fuel)
where n
is the number of cities, as we have at most n × fuel
states and each state examines n
cities.
Space Complexity: O(n × fuel)
for the memoization cache and recursion stack.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example Input:
locations = [2, 3, 6, 8, 4]
(city positions)start = 1
(starting at city index 1, position 3)finish = 3
(destination at city index 3, position 8)fuel = 5
Step-by-Step DFS Exploration:
Starting with dfs(1, 5)
- we're at city 1 (position 3) with 5 fuel:
-
Early termination check: Distance to finish = |3 - 8| = 5. We have exactly 5 fuel, so we can potentially reach it.
-
Initialize count: Since we're not at finish (1 ≠ 3),
ans = 0
-
Explore moves from city 1:
- To city 0 (position 2): costs |3 - 2| = 1 fuel → call
dfs(0, 4)
- To city 2 (position 6): costs |3 - 6| = 3 fuel → call
dfs(2, 2)
- To city 3 (position 8): costs |3 - 8| = 5 fuel → call
dfs(3, 0)
- To city 4 (position 4): costs |3 - 4| = 1 fuel → call
dfs(4, 4)
- To city 0 (position 2): costs |3 - 2| = 1 fuel → call
Following one branch - dfs(3, 0)
:
- We're at city 3 (the finish!) with 0 fuel
- Early termination check: Distance to finish = 0, we have 0 fuel, so we pass
- Initialize count: Since we're at finish,
ans = 1
(found one route!) - Try to explore moves: With 0 fuel, we can't move anywhere
- Return 1
Following another branch - dfs(0, 4)
:
- We're at city 0 (position 2) with 4 fuel
- Early termination check: Distance to finish = |2 - 8| = 6, but we only have 4 fuel
- This means we can never reach the finish from here
- Return 0 immediately
Following dfs(2, 2)
:
- We're at city 2 (position 6) with 2 fuel
- Early termination check: Distance to finish = |6 - 8| = 2, we have exactly 2 fuel
- Initialize count: Not at finish, so
ans = 0
- Explore moves:
- To city 3 (position 8): costs |6 - 8| = 2 fuel → call
dfs(3, 0)
(returns 1) - Other moves cost too much fuel
- To city 3 (position 8): costs |6 - 8| = 2 fuel → call
- Return 1
Memoization in Action:
When we call dfs(3, 0)
from different paths (like from city 1 directly or via city 2), the second call returns the cached result immediately without recalculation.
Final Result: The total count accumulates from all valid paths. In this example, we find 2 valid routes:
- City 1 → City 3 (direct route using all 5 fuel)
- City 1 → City 2 → City 3 (using 3 + 2 = 5 fuel)
The function returns 2 as the total number of valid routes.
Solution Implementation
1class Solution:
2 def countRoutes(
3 self, locations: List[int], start: int, finish: int, fuel: int
4 ) -> int:
5 MOD = 10**9 + 7
6
7 @cache
8 def dfs(current_city: int, remaining_fuel: int) -> int:
9 """
10 Calculate the number of routes from current_city to finish with remaining_fuel.
11
12 Args:
13 current_city: Index of the current city in locations array
14 remaining_fuel: Amount of fuel remaining
15
16 Returns:
17 Number of valid routes modulo MOD
18 """
19 # If we don't have enough fuel to reach the finish city even directly,
20 # there are no valid routes from here
21 if remaining_fuel < abs(locations[current_city] - locations[finish]):
22 return 0
23
24 # Initialize route count - if we're at the finish, this counts as one route
25 route_count = 1 if current_city == finish else 0
26
27 # Try moving to every other city
28 for next_city in range(len(locations)):
29 if next_city != current_city:
30 # Calculate fuel cost to move to next_city
31 fuel_cost = abs(locations[current_city] - locations[next_city])
32
33 # Recursively count routes from next_city with reduced fuel
34 route_count = (route_count + dfs(next_city, remaining_fuel - fuel_cost)) % MOD
35
36 return route_count
37
38 # Start the DFS from the starting city with full fuel
39 return dfs(start, fuel)
40
1class Solution {
2 // Array to store city locations
3 private int[] locations;
4 // Target city index
5 private int finish;
6 // Total number of cities
7 private int n;
8 // Memoization table: dp[cityIndex][remainingFuel]
9 private Integer[][] dp;
10 // Modulo value for result
11 private final int MOD = (int) 1e9 + 7;
12
13 /**
14 * Counts the number of possible routes from start to finish with given fuel limit.
15 *
16 * @param locations Array of city locations
17 * @param start Starting city index
18 * @param finish Target city index
19 * @param fuel Initial fuel amount
20 * @return Number of possible routes modulo 10^9 + 7
21 */
22 public int countRoutes(int[] locations, int start, int finish, int fuel) {
23 this.n = locations.length;
24 this.locations = locations;
25 this.finish = finish;
26 this.dp = new Integer[n][fuel + 1];
27
28 return dfs(start, fuel);
29 }
30
31 /**
32 * Depth-first search with memoization to count valid routes.
33 *
34 * @param currentCity Current city index
35 * @param remainingFuel Remaining fuel amount
36 * @return Number of valid routes from current state
37 */
38 private int dfs(int currentCity, int remainingFuel) {
39 // Early termination: not enough fuel to reach finish city
40 int minFuelNeeded = Math.abs(locations[currentCity] - locations[finish]);
41 if (remainingFuel < minFuelNeeded) {
42 return 0;
43 }
44
45 // Return memoized result if already computed
46 if (dp[currentCity][remainingFuel] != null) {
47 return dp[currentCity][remainingFuel];
48 }
49
50 // Initialize result: add 1 if currently at finish city
51 int result = (currentCity == finish) ? 1 : 0;
52
53 // Try moving to all other cities
54 for (int nextCity = 0; nextCity < n; nextCity++) {
55 if (nextCity != currentCity) {
56 int fuelCost = Math.abs(locations[currentCity] - locations[nextCity]);
57 result = (result + dfs(nextCity, remainingFuel - fuelCost)) % MOD;
58 }
59 }
60
61 // Memoize and return result
62 dp[currentCity][remainingFuel] = result;
63 return result;
64 }
65}
66
1class Solution {
2public:
3 int countRoutes(vector<int>& locations, int start, int finish, int fuel) {
4 int n = locations.size();
5
6 // dp[i][j] represents the number of routes from city i with j fuel remaining
7 // -1 indicates unvisited state
8 int dp[n][fuel + 1];
9 memset(dp, -1, sizeof(dp));
10
11 const int MOD = 1e9 + 7;
12
13 // DFS with memoization to count all possible routes
14 function<int(int, int)> dfs = [&](int currentCity, int remainingFuel) -> int {
15 // Optimization: if we don't have enough fuel to reach the finish city directly,
16 // we can't reach it at all from current position
17 if (remainingFuel < abs(locations[currentCity] - locations[finish])) {
18 return 0;
19 }
20
21 // Return memoized result if already computed
22 if (dp[currentCity][remainingFuel] != -1) {
23 return dp[currentCity][remainingFuel];
24 }
25
26 // Initialize route count: add 1 if we're at the finish city
27 int routeCount = (currentCity == finish) ? 1 : 0;
28
29 // Try moving to every other city
30 for (int nextCity = 0; nextCity < n; ++nextCity) {
31 if (nextCity != currentCity) {
32 int fuelCost = abs(locations[currentCity] - locations[nextCity]);
33 // Recursively count routes from the next city with reduced fuel
34 routeCount = (routeCount + dfs(nextCity, remainingFuel - fuelCost)) % MOD;
35 }
36 }
37
38 // Memoize and return the result
39 return dp[currentCity][remainingFuel] = routeCount;
40 };
41
42 // Start DFS from the starting city with full fuel
43 return dfs(start, fuel);
44 }
45};
46
1/**
2 * Counts the number of routes from start to finish with given fuel constraint
3 * Uses dynamic programming with memoization to avoid recalculating subproblems
4 *
5 * @param locations - Array of location positions
6 * @param start - Starting location index
7 * @param finish - Destination location index
8 * @param fuel - Available fuel amount
9 * @returns Number of possible routes modulo 10^9 + 7
10 */
11function countRoutes(locations: number[], start: number, finish: number, fuel: number): number {
12 const locationCount: number = locations.length;
13
14 // Memoization table: memo[currentLocation][remainingFuel] = number of routes
15 // Initialize with -1 to indicate uncomputed states
16 const memo: number[][] = Array.from(
17 { length: locationCount },
18 () => Array(fuel + 1).fill(-1)
19 );
20
21 const MODULO: number = 1e9 + 7;
22
23 /**
24 * Depth-first search with memoization to count valid routes
25 *
26 * @param currentLocation - Current location index
27 * @param remainingFuel - Remaining fuel amount
28 * @returns Number of valid routes from current state to finish
29 */
30 const dfs = (currentLocation: number, remainingFuel: number): number => {
31 // Pruning: If remaining fuel cannot reach finish location, no valid routes exist
32 const minimumFuelNeeded: number = Math.abs(locations[currentLocation] - locations[finish]);
33 if (remainingFuel < minimumFuelNeeded) {
34 return 0;
35 }
36
37 // Return memoized result if already computed
38 if (memo[currentLocation][remainingFuel] !== -1) {
39 return memo[currentLocation][remainingFuel];
40 }
41
42 // Initialize route count: if at finish location, count this as one valid route
43 let routeCount: number = currentLocation === finish ? 1 : 0;
44
45 // Try moving to all other locations
46 for (let nextLocation = 0; nextLocation < locationCount; nextLocation++) {
47 // Skip if trying to stay at same location
48 if (nextLocation !== currentLocation) {
49 // Calculate fuel cost to move to next location
50 const fuelCost: number = Math.abs(locations[currentLocation] - locations[nextLocation]);
51
52 // Recursively count routes from next location with reduced fuel
53 routeCount = (routeCount + dfs(nextLocation, remainingFuel - fuelCost)) % MODULO;
54 }
55 }
56
57 // Memoize and return the result
58 memo[currentLocation][remainingFuel] = routeCount;
59 return routeCount;
60 };
61
62 // Start the search from initial location with full fuel
63 return dfs(start, fuel);
64}
65
Time and Space Complexity
The time complexity is O(n^2 × m)
, where n
is the size of the array locations
and m
is the value of fuel
.
Time Complexity Analysis:
- The recursive function
dfs(i, k)
has at mostn × m
unique states, wherei
ranges from0
ton-1
(representing different locations) andk
ranges from0
tom
(representing remaining fuel). - Due to the
@cache
decorator, each state is computed only once. - For each state
(i, k)
, the function iterates through alln
locations to find possible next destinations, takingO(n)
time. - Therefore, the total time complexity is
O(n × m) × O(n) = O(n^2 × m)
.
The space complexity is O(n × m)
.
Space Complexity Analysis:
- The
@cache
decorator stores the results of all computed states, which requiresO(n × m)
space to store all possible combinations of location index and remaining fuel. - The recursion depth in the worst case could be
O(m)
(when we use 1 unit of fuel at each step), but this is dominated by the cache storage. - Therefore, the total space complexity is
O(n × m)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Early Termination Logic
The Pitfall:
A common mistake is to terminate early when remaining_fuel < abs(locations[current_city] - locations[finish])
, thinking no valid routes exist. However, this optimization is actually incorrect for this problem.
Why It's Wrong: The finish city might not be the farthest city from our current position. Even if we can't reach the finish directly, we might be able to reach it through intermediate cities that are closer to us.
Example:
- Locations: [1, 5, 3], start=0 (position 1), finish=2 (position 3), fuel=3
- From city 0, direct distance to finish is |1-3|=2, which is feasible
- But if we had fuel=1, direct route is impossible (needs 2 fuel)
- However, we could still go 0→1 (cost |1-5|=4), which exceeds our fuel
- The early termination would incorrectly return 0 for some valid paths through closer intermediate cities
The Fix: Remove the early termination check entirely or make it more sophisticated:
@cache
def dfs(current_city: int, remaining_fuel: int) -> int:
# Remove the incorrect early termination
# if remaining_fuel < abs(locations[current_city] - locations[finish]):
# return 0
route_count = 1 if current_city == finish else 0
for next_city in range(len(locations)):
if next_city != current_city:
fuel_cost = abs(locations[current_city] - locations[next_city])
# Only skip if we don't have enough fuel for this specific move
if fuel_cost <= remaining_fuel:
route_count = (route_count + dfs(next_city, remaining_fuel - fuel_cost)) % MOD
return route_count
2. Forgetting to Check Fuel Before Making a Move
The Pitfall:
Calling dfs(next_city, remaining_fuel - fuel_cost)
without first checking if fuel_cost <= remaining_fuel
, which could lead to exploring states with negative fuel.
Why It's Wrong: The problem explicitly states you cannot make a move if it would cause your fuel to become negative. Exploring negative fuel states violates the problem constraints.
The Fix: Always validate fuel sufficiency before making a recursive call:
for next_city in range(len(locations)):
if next_city != current_city:
fuel_cost = abs(locations[current_city] - locations[next_city])
if fuel_cost <= remaining_fuel: # Check fuel sufficiency first
route_count = (route_count + dfs(next_city, remaining_fuel - fuel_cost)) % MOD
3. Forgetting the Modulo Operation
The Pitfall: Not applying modulo at each addition step, only applying it at the final return:
# Wrong approach
route_count = 1 if current_city == finish else 0
for next_city in range(len(locations)):
if next_city != current_city and fuel_cost <= remaining_fuel:
route_count += dfs(next_city, remaining_fuel - fuel_cost)
return route_count % MOD # Only modulo at the end
Why It's Wrong: Integer overflow can occur during intermediate calculations before the final modulo operation, especially in languages with fixed integer sizes. Even in Python with arbitrary precision integers, it's good practice to apply modulo consistently.
The Fix: Apply modulo after each addition to keep numbers manageable:
route_count = (route_count + dfs(next_city, remaining_fuel - fuel_cost)) % MOD
What data structure does Breadth-first search typically uses to store intermediate states?
Recommended Readings
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!