2093. Minimum Cost to Reach City With Discounts 🔒
Problem Description
You have n
cities numbered from 0
to n - 1
connected by highways. Each highway is bidirectional and has a toll cost. You're given:
highways[i] = [city1, city2, toll]
: a highway connectingcity1
andcity2
with costtoll
discounts
: the number of discount coupons you have
You need to find the minimum cost to travel from city 0
to city n - 1
.
Key rules for using discounts:
- Each discount reduces a highway's toll to
toll / 2
(integer division) - You can use at most one discount per highway
- Each discount can only be used once
- You have a total of
discounts
number of discounts available
The problem asks you to return:
- The minimum total cost to reach city
n - 1
from city0
- Return
-1
if it's impossible to reach cityn - 1
from city0
Example breakdown:
- In Example 1, with 1 discount available, the optimal path is
0 → 1
(cost 4) then1 → 4
with discount (cost 11/2 = 5), totaling 9. - In Example 2, with 20 discounts (more than needed), you can use discounts on every highway in the path
0 → 1 → 2 → 3
, getting costs of 3 + 3 + 2 = 8. - In Example 3, there's no path connecting city 0 to city 3, so the answer is -1.
This is essentially a shortest path problem with the added complexity of managing a limited number of discounts that can reduce edge weights by half.
Intuition
This problem is a variation of the classic shortest path problem, but with an extra dimension - we can modify edge weights by using discounts. The key insight is that we need to track not just "what's the shortest path to each city" but rather "what's the shortest path to each city using exactly k discounts."
Think of it this way: when we're at a city, we have multiple states based on how many discounts we've used so far. For example, reaching city 2 with 0 discounts used might cost 10, but reaching city 2 with 1 discount used might cost 7. These are different states that could lead to different optimal paths to the destination.
This naturally leads us to think about Dijkstra's algorithm, but instead of maintaining a single distance value for each node, we maintain a 2D array dist[city][discounts_used]
that tracks the minimum cost to reach each city with a specific number of discounts used.
When we explore from a current city, we have two choices for each neighboring highway:
- Take the highway at full price (don't use a discount)
- Take the highway at half price (use a discount if we have any left)
Both choices create new states: (next_city, same_discount_count)
and (next_city, discount_count + 1)
. We explore all these states using a priority queue, always processing the lowest cost state first.
The algorithm terminates when we reach city n-1
for the first time (since we're using Dijkstra's with a min-heap, the first time we reach the destination is guaranteed to be the minimum cost), or when we've exhausted all reachable states without reaching the destination (returning -1).
The beauty of this approach is that it automatically finds the optimal balance between using discounts on expensive highways versus saving them for later, because it explores all possible combinations in order of increasing cost.
Learn more about Graph, Shortest Path and Heap (Priority Queue) patterns.
Solution Approach
The solution implements a modified Dijkstra's algorithm with state tracking for discount usage. Let's walk through the implementation step by step:
1. Build the Graph:
g = defaultdict(list)
for a, b, c in highways:
g[a].append((b, c))
g[b].append((a, c))
We create an adjacency list representation of the graph. Since highways are bidirectional, we add edges in both directions.
2. Initialize Data Structures:
q = [(0, 0, 0)] # (cost, city, discounts_used)
dist = [[inf] * (discounts + 1) for _ in range(n)]
q
: A min-heap (priority queue) initialized with the starting state: cost=0, city=0, discounts_used=0dist[i][k]
: A 2D array wheredist[i][k]
represents the minimum cost to reach cityi
using exactlyk
discounts
3. Process States with Dijkstra's Algorithm:
while q: cost, i, k = heappop(q) if k > discounts: continue if i == n - 1: return cost
We continuously extract the state with minimum cost. If we've used too many discounts, skip this state. If we've reached the destination city n-1
, return the cost immediately (first arrival guarantees minimum cost due to the priority queue).
4. Update Distances and Explore Neighbors:
if dist[i][k] > cost: dist[i][k] = cost for j, v in g[i]: heappush(q, (cost + v, j, k)) # Don't use discount heappush(q, (cost + v // 2, j, k + 1)) # Use discount
Only process this state if it offers a better cost than previously recorded. For each neighbor j
with toll v
:
- Add state without using discount:
(cost + v, j, k)
- Add state with discount:
(cost + v // 2, j, k + 1)
wherev // 2
is integer division
5. Handle Unreachable Case:
return -1
If the queue is exhausted without reaching city n-1
, the destination is unreachable.
Time Complexity: O((V * D + E * D) * log(V * D))
where V is the number of cities, E is the number of highways, and D is the number of discounts. Each state (city, discounts_used)
can be pushed to the heap multiple times, and heap operations take logarithmic time.
Space Complexity: O(V * D)
for the distance array plus the heap size which can grow up to O(E * D)
in the worst case.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example:
- Cities: 0, 1, 2, 3 (n = 4)
- Highways:
[[0,1,5], [1,2,7], [0,2,10], [2,3,3]]
- Discounts: 1
- Goal: Find minimum cost from city 0 to city 3
Step-by-step execution:
Initial Setup:
- Graph adjacency list:
- 0: [(1,5), (2,10)]
- 1: [(0,5), (2,7)]
- 2: [(1,7), (0,10), (3,3)]
- 3: [(2,3)]
- Priority queue:
[(0, 0, 0)]
(cost=0, city=0, discounts_used=0) - Distance array: All initialized to infinity
Iteration 1: Pop (0, 0, 0)
- Current state: cost=0, city=0, discounts_used=0
- Update
dist[0][0] = 0
- Add neighbors:
- To city 1:
(5, 1, 0)
without discount,(2, 1, 1)
with discount - To city 2:
(10, 2, 0)
without discount,(5, 2, 1)
with discount
- To city 1:
- Queue:
[(2,1,1), (5,1,0), (5,2,1), (10,2,0)]
Iteration 2: Pop (2, 1, 1)
- Current state: cost=2, city=1, discounts_used=1
- Update
dist[1][1] = 2
- Add neighbors:
- To city 0:
(7, 0, 1)
without discount (skip discount, already used 1) - To city 2:
(9, 2, 1)
without discount (skip discount option)
- To city 0:
- Queue:
[(5,1,0), (5,2,1), (7,0,1), (9,2,1), (10,2,0)]
Iteration 3: Pop (5, 1, 0)
- Current state: cost=5, city=1, discounts_used=0
- Update
dist[1][0] = 5
- Add neighbors:
- To city 0:
(10, 0, 0)
without discount,(7, 0, 1)
with discount - To city 2:
(12, 2, 0)
without discount,(8, 2, 1)
with discount
- To city 0:
- Queue:
[(5,2,1), (7,0,1), (8,2,1), (9,2,1), (10,0,0), (10,2,0), (12,2,0)]
Iteration 4: Pop (5, 2, 1)
- Current state: cost=5, city=2, discounts_used=1
- Update
dist[2][1] = 5
- Add neighbors:
- To city 1:
(12, 1, 1)
without discount - To city 0:
(15, 0, 1)
without discount - To city 3:
(8, 3, 1)
without discount (can't use more discounts)
- To city 1:
- Queue:
[(7,0,1), (8,2,1), (8,3,1), (9,2,1), (10,0,0), (10,2,0), (12,1,1), (12,2,0), (15,0,1)]
Iteration 5: Pop (7, 0, 1)
- Already visited
dist[0][1] = 7
(not better than infinity, so process) - Skip for brevity...
Iteration 6: Pop (8, 2, 1)
- Cost 8 >
dist[2][1] = 5
, skip
Iteration 7: Pop (8, 3, 1)
- Reached destination city 3!
- Return cost = 8
Final Answer: The minimum cost is 8
- Optimal path: 0 → 1 (using discount: 5/2=2) → 2 (no discount: 7) → 3 (no discount: 3)
- Total: 2 + 7 + 3 = 12 (wait, let's recalculate...)
Actually, let me trace the optimal path more carefully:
- 0 → 2 (using discount: 10/2=5) → 3 (no discount: 3)
- Total: 5 + 3 = 8 ✓
The algorithm correctly finds that using our single discount on the expensive 0→2 highway (reducing it from 10 to 5) and then taking the 2→3 highway at full price gives us the minimum total cost of 8.
Solution Implementation
1from collections import defaultdict
2from heapq import heappush, heappop
3from typing import List
4
5class Solution:
6 def minimumCost(self, n: int, highways: List[List[int]], discounts: int) -> int:
7 # Build adjacency list representation of the graph
8 graph = defaultdict(list)
9 for city1, city2, toll in highways:
10 graph[city1].append((city2, toll))
11 graph[city2].append((city1, toll))
12
13 # Priority queue: (cost, current_city, discounts_used)
14 priority_queue = [(0, 0, 0)]
15
16 # Distance matrix: dist[city][discounts_used] = minimum cost
17 # Initialize with infinity for all states
18 distance = [[float('inf')] * (discounts + 1) for _ in range(n)]
19
20 # Dijkstra's algorithm with state (city, discounts_used)
21 while priority_queue:
22 current_cost, current_city, discounts_used = heappop(priority_queue)
23
24 # Skip if we've exceeded the discount limit
25 if discounts_used > discounts:
26 continue
27
28 # Return cost if we've reached the destination (city n-1)
29 if current_city == n - 1:
30 return current_cost
31
32 # Only process if this is a better path than previously found
33 if distance[current_city][discounts_used] > current_cost:
34 distance[current_city][discounts_used] = current_cost
35
36 # Explore all neighboring cities
37 for next_city, toll_cost in graph[current_city]:
38 # Option 1: Travel without using a discount
39 heappush(priority_queue, (current_cost + toll_cost, next_city, discounts_used))
40
41 # Option 2: Travel using a discount (half price)
42 heappush(priority_queue, (current_cost + toll_cost // 2, next_city, discounts_used + 1))
43
44 # No path found from city 0 to city n-1
45 return -1
46
1class Solution {
2 public int minimumCost(int n, int[][] highways, int discounts) {
3 // Build adjacency list representation of the graph
4 List<int[]>[] graph = new List[n];
5 for (int i = 0; i < n; i++) {
6 graph[i] = new ArrayList<>();
7 }
8
9 // Add edges to the graph (undirected)
10 for (int[] highway : highways) {
11 int city1 = highway[0];
12 int city2 = highway[1];
13 int toll = highway[2];
14 graph[city1].add(new int[] {city2, toll});
15 graph[city2].add(new int[] {city1, toll});
16 }
17
18 // Priority queue for Dijkstra's algorithm: [cost, city, discountsUsed]
19 PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((a, b) -> a[0] - b[0]);
20 priorityQueue.offer(new int[] {0, 0, 0}); // Start from city 0 with 0 cost and 0 discounts used
21
22 // Distance array: dist[city][discountsUsed] = minimum cost to reach city with exactly discountsUsed
23 int[][] distance = new int[n][discounts + 1];
24 for (int[] row : distance) {
25 Arrays.fill(row, Integer.MAX_VALUE);
26 }
27
28 // Dijkstra's algorithm with state (city, discounts used)
29 while (!priorityQueue.isEmpty()) {
30 int[] current = priorityQueue.poll();
31 int currentCost = current[0];
32 int currentCity = current[1];
33 int discountsUsed = current[2];
34
35 // Skip if we've exceeded discount limit or found a better path already
36 if (discountsUsed > discounts || distance[currentCity][discountsUsed] <= currentCost) {
37 continue;
38 }
39
40 // Check if we've reached the destination (city n-1)
41 if (currentCity == n - 1) {
42 return currentCost;
43 }
44
45 // Update the minimum distance for this state
46 distance[currentCity][discountsUsed] = currentCost;
47
48 // Explore neighboring cities
49 for (int[] neighbor : graph[currentCity]) {
50 int nextCity = neighbor[0];
51 int tollCost = neighbor[1];
52
53 // Option 1: Pay full toll
54 priorityQueue.offer(new int[] {currentCost + tollCost, nextCity, discountsUsed});
55
56 // Option 2: Use a discount (pay half toll)
57 priorityQueue.offer(new int[] {currentCost + tollCost / 2, nextCity, discountsUsed + 1});
58 }
59 }
60
61 // No path found from city 0 to city n-1
62 return -1;
63 }
64}
65
1class Solution {
2public:
3 int minimumCost(int n, vector<vector<int>>& highways, int discounts) {
4 // Build adjacency list representation of the graph
5 // graph[node] = list of {neighbor, edge_cost}
6 vector<vector<pair<int, int>>> graph(n);
7 for (auto& edge : highways) {
8 int node1 = edge[0];
9 int node2 = edge[1];
10 int cost = edge[2];
11
12 // Add bidirectional edges
13 graph[node1].push_back({node2, cost});
14 graph[node2].push_back({node1, cost});
15 }
16
17 // Min-heap priority queue: {total_cost, current_node, discounts_used}
18 // Ordered by total_cost (smallest first)
19 priority_queue<tuple<int, int, int>,
20 vector<tuple<int, int, int>>,
21 greater<tuple<int, int, int>>> minHeap;
22
23 // Start from node 0 with cost 0 and 0 discounts used
24 minHeap.push({0, 0, 0});
25
26 // distance[node][discounts_used] = minimum cost to reach 'node' using exactly 'discounts_used' discounts
27 vector<vector<int>> distance(n, vector<int>(discounts + 1, INT_MAX));
28
29 // Modified Dijkstra's algorithm with state (node, discounts_used)
30 while (!minHeap.empty()) {
31 // Extract the state with minimum cost
32 auto [currentCost, currentNode, discountsUsed] = minHeap.top();
33 minHeap.pop();
34
35 // Skip if we've exceeded discount limit or found a better path already
36 if (discountsUsed > discounts || distance[currentNode][discountsUsed] <= currentCost) {
37 continue;
38 }
39
40 // If we reached the destination (node n-1), return the cost
41 if (currentNode == n - 1) {
42 return currentCost;
43 }
44
45 // Update the minimum distance for this state
46 distance[currentNode][discountsUsed] = currentCost;
47
48 // Explore all neighbors
49 for (auto [neighbor, edgeCost] : graph[currentNode]) {
50 // Option 1: Use the edge without discount
51 minHeap.push({currentCost + edgeCost, neighbor, discountsUsed});
52
53 // Option 2: Use the edge with discount (half price)
54 minHeap.push({currentCost + edgeCost / 2, neighbor, discountsUsed + 1});
55 }
56 }
57
58 // No path found from node 0 to node n-1
59 return -1;
60 }
61};
62
1function minimumCost(n: number, highways: number[][], discounts: number): number {
2 // Build adjacency list representation of the graph
3 // graph[node] = list of [neighbor, edgeCost]
4 const graph: number[][][] = Array(n).fill(null).map(() => []);
5
6 for (const edge of highways) {
7 const node1 = edge[0];
8 const node2 = edge[1];
9 const cost = edge[2];
10
11 // Add bidirectional edges
12 graph[node1].push([node2, cost]);
13 graph[node2].push([node1, cost]);
14 }
15
16 // Min-heap priority queue: [totalCost, currentNode, discountsUsed]
17 // Using array as heap, will be sorted by totalCost (smallest first)
18 const minHeap: number[][] = [];
19
20 // Helper function to maintain min-heap property
21 const heapPush = (item: number[]): void => {
22 minHeap.push(item);
23 minHeap.sort((a, b) => a[0] - b[0]);
24 };
25
26 const heapPop = (): number[] | undefined => {
27 return minHeap.shift();
28 };
29
30 // Start from node 0 with cost 0 and 0 discounts used
31 heapPush([0, 0, 0]);
32
33 // distance[node][discountsUsed] = minimum cost to reach 'node' using exactly 'discountsUsed' discounts
34 const distance: number[][] = Array(n).fill(null).map(() =>
35 Array(discounts + 1).fill(Number.MAX_SAFE_INTEGER)
36 );
37
38 // Modified Dijkstra's algorithm with state (node, discountsUsed)
39 while (minHeap.length > 0) {
40 // Extract the state with minimum cost
41 const current = heapPop();
42 if (!current) break;
43
44 const [currentCost, currentNode, discountsUsed] = current;
45
46 // Skip if we've exceeded discount limit or found a better path already
47 if (discountsUsed > discounts || distance[currentNode][discountsUsed] <= currentCost) {
48 continue;
49 }
50
51 // If we reached the destination (node n-1), return the cost
52 if (currentNode === n - 1) {
53 return currentCost;
54 }
55
56 // Update the minimum distance for this state
57 distance[currentNode][discountsUsed] = currentCost;
58
59 // Explore all neighbors
60 for (const [neighbor, edgeCost] of graph[currentNode]) {
61 // Option 1: Use the edge without discount
62 heapPush([currentCost + edgeCost, neighbor, discountsUsed]);
63
64 // Option 2: Use the edge with discount (half price, rounded down)
65 heapPush([currentCost + Math.floor(edgeCost / 2), neighbor, discountsUsed + 1]);
66 }
67 }
68
69 // No path found from node 0 to node n-1
70 return -1;
71}
72
Time and Space Complexity
Time Complexity: O(n * k * m * log(n * k * m))
where n
is the number of nodes, k
is the number of discounts, and m
is the number of edges (highways).
- The algorithm uses Dijkstra's algorithm with a modified state space that includes both the current node and the number of discounts used.
- Each state
(node, discounts_used)
can be visited, creating up ton * (k + 1)
possible states. - For each state, we explore all adjacent edges. In the worst case, a node can have up to
m
edges. - Each edge exploration results in two heap push operations (one with discount, one without).
- This gives us up to
2 * m * n * (k + 1)
heap operations. - Each heap operation (push/pop) takes
O(log(heap_size))
time, where the heap can grow up toO(n * k * m)
in size. - Therefore, the overall time complexity is
O(n * k * m * log(n * k * m))
.
Space Complexity: O(n * k + m + n * k * m)
- The
dist
array usesO(n * (k + 1))
=O(n * k)
space to store the minimum cost for each(node, discounts_used)
combination. - The adjacency list
g
stores all edges, usingO(m)
space where each edge is stored twice (undirected graph). - The priority queue
q
can contain up toO(n * k * m)
elements in the worst case, as each state can generate multiple entries before being processed. - Therefore, the total space complexity is
O(n * k + m + n * k * m)
which simplifies toO(n * k * m)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Handling the Discount Limit Properly
A common mistake is allowing states where discounts_used
exceeds the available discounts
. While the current code checks this condition (if discounts_used > discounts: continue
), a more efficient approach would be to prevent these invalid states from being added to the queue in the first place.
Problem: The current implementation adds states with discounts_used + 1
even when discounts_used
might already equal discounts
, creating unnecessary states in the queue.
Solution:
# Explore all neighboring cities for next_city, toll_cost in graph[current_city]: # Option 1: Travel without using a discount heappush(priority_queue, (current_cost + toll_cost, next_city, discounts_used)) # Option 2: Travel using a discount (only if we have discounts left) if discounts_used < discounts: # Add this check heappush(priority_queue, (current_cost + toll_cost // 2, next_city, discounts_used + 1))
2. Revisiting Already Processed States
The current implementation checks if a state offers a better cost before processing its neighbors, but it doesn't prevent revisiting already optimally processed states. This can lead to redundant computations.
Problem: A state (city, discounts_used)
might be processed multiple times if it appears in the queue with different costs.
Solution: Add a visited set to track processed states:
visited = set()
while priority_queue:
current_cost, current_city, discounts_used = heappop(priority_queue)
# Skip if already processed this state optimally
if (current_city, discounts_used) in visited:
continue
visited.add((current_city, discounts_used))
# Rest of the logic...
3. Integer Division Edge Case
When using a discount, the toll becomes toll // 2
. For odd tolls, this rounds down (e.g., 5 // 2 = 2), which is correct per the problem specification. However, some might mistakenly use regular division or forget about the rounding behavior.
Problem: Using toll / 2
instead of toll // 2
would result in float values and incorrect calculations.
Solution: Always use integer division (//
) when applying discounts, as shown in the correct implementation.
4. Initializing Distance Array Incorrectly
The distance array needs to track the minimum cost for each combination of (city, discounts_used)
. A common mistake is creating a 1D array or incorrectly sized 2D array.
Problem: Using dist = [float('inf')] * n
or dist = [[float('inf')] * discounts for _ in range(n)]
(missing the +1).
Solution: The array should be (discounts + 1)
in the second dimension to account for states from 0 to discounts
discounts used:
distance = [[float('inf')] * (discounts + 1) for _ in range(n)]
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Shortest Path Between A and B Prereq BFS on Graph problems graph_bfs Given an unweighted connected graph return the length of the shortest path between two nodes A and B in terms of the number of edges Assume there always exists a path between nodes A and B Input graph
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Want a Structured Path to Master System Design Too? Don’t Miss This!