2473. Minimum Cost to Buy Apples 🔒
Problem Description
You have n
cities numbered from 1
to n
. These cities are connected by bidirectional roads given in a 2D array roads
, where roads[i] = [ai, bi, costi]
means there's a road between city ai
and city bi
with travel cost costi
.
Each city sells apples at different prices. The array appleCost
tells you the price of an apple in each city, where appleCost[i]
is the cost to buy one apple from city i
(using 0-based indexing in the actual implementation).
Here's the task:
- Start from any city
- Travel through roads to reach any city (could be the starting city itself)
- Buy exactly one apple from that city
- Return back to your starting city
The catch is that after buying the apple, all road costs are multiplied by a factor k
for your return journey. So if a road originally costs c
, it will cost c * k
on the way back.
Given the multiplier k
, you need to find the minimum total cost for each possible starting city. Return an array answer
where answer[i]
represents the minimum cost to complete the entire journey (go somewhere, buy an apple, and return) if you start from city i
.
The total cost for a journey includes:
- Cost to travel from starting city to the city where you buy the apple
- Cost of the apple itself
- Cost to travel back (with road costs multiplied by
k
)
Since the same roads are used for going and returning, if the shortest path from city A to city B costs d
, then the total travel cost would be d + d * k = d * (1 + k)
.
Intuition
The key insight is recognizing that we need to find the optimal city to buy an apple from, for each possible starting city. Since we must return to our starting point after buying the apple, and the return journey costs are multiplied by k
, the total travel cost becomes distance * (1 + k)
.
Think about what happens when we start from city i
:
- We need to reach some city
j
to buy an apple - The shortest path from
i
toj
has some costd
- We buy an apple at city
j
forappleCost[j]
- We return via the same shortest path, but now it costs
d * k
- Total cost =
d + appleCost[j] + d * k = d * (1 + k) + appleCost[j]
This means for each starting city, we want to minimize: distance_to_city * (1 + k) + apple_cost_at_city
Since we need to evaluate this for every possible destination city from each starting point, we need the shortest distances from each starting city to all other cities. This naturally leads us to use Dijkstra's algorithm.
Why Dijkstra? Because:
- We need shortest paths from a single source (the starting city) to all other cities
- All edge weights (road costs) are positive
- We can calculate the minimum cost on-the-fly as we explore cities
During the Dijkstra traversal from starting city i
, when we reach any city u
with shortest distance d
, we can immediately calculate the total cost if we buy an apple there: d * (k + 1) + appleCost[u]
. We keep track of the minimum among all such costs.
The beauty of this approach is that Dijkstra guarantees we visit each city with its shortest distance from the source, so we can confidently calculate the cost for buying an apple at each city and maintain the global minimum.
Learn more about Graph, Shortest Path and Heap (Priority Queue) patterns.
Solution Approach
The solution implements a heap-optimized Dijkstra's algorithm for each city as the starting point. Here's how the implementation works:
Graph Construction: First, we build an adjacency list representation of the graph:
g = defaultdict(list)
for a, b, c in roads:
a, b = a - 1, b - 1 # Convert to 0-based indexing
g[a].append((b, c))
g[b].append((a, c))
Since roads are bidirectional, we add edges in both directions. The cities are converted from 1-based to 0-based indexing for easier array manipulation.
Dijkstra's Algorithm for Each Starting City:
For each city i
from 0
to n-1
, we run a modified Dijkstra's algorithm:
def dijkstra(i):
q = [(0, i)] # Priority queue: (distance, city)
dist = [inf] * n # Shortest distances from city i
dist[i] = 0
ans = inf # Minimum cost to complete the journey
The algorithm uses:
- A min-heap
q
to always process the city with minimum distance first - A
dist
array to track the shortest distance from starting cityi
to all other cities - An
ans
variable to track the minimum total cost found so far
Core Algorithm Logic:
while q:
d, u = heappop(q)
ans = min(ans, appleCost[u] + d * (k + 1))
for v, w in g[u]:
if dist[v] > dist[u] + w:
dist[v] = dist[u] + w
heappush(q, (dist[v], v))
For each city u
we visit:
- Calculate the total cost if we buy an apple here:
appleCost[u] + d * (k + 1)
d
is the shortest distance from starting city tou
d * (k + 1)
represents the round-trip cost (going costsd
, returning costsd * k
)
- Update
ans
with the minimum cost found - Explore neighbors and update their distances if we found a shorter path
- Add updated neighbors to the priority queue
Final Result:
return [dijkstra(i) for i in range(n)]
We run Dijkstra from each city and collect all minimum costs in a list.
Time Complexity: O(n * (n + m) * log n)
where n
is the number of cities and m
is the number of roads. We run Dijkstra n
times, and each run takes O((n + m) * log n)
with the heap operations.
Space Complexity: O(n + m)
for storing the graph and the distance array.
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.
Given:
n = 4
cities (numbered 1 to 4)roads = [[1,2,4], [2,3,2], [2,4,5], [3,4,1]]
appleCost = [56, 42, 86, 23]
(0-indexed: city 1 costs 56, city 2 costs 42, etc.)k = 2
(return journey costs are doubled)
Graph Visualization:
1 --4-- 2 /|\ 2 | 5 \|/ 3 --1-- 4
Step 1: Convert to 0-based indexing and build adjacency list
City 0: [(1, 4)] City 1: [(0, 4), (2, 2), (3, 5)] City 2: [(1, 2), (3, 1)] City 3: [(1, 5), (2, 1)]
Step 2: Run Dijkstra from City 0 (originally City 1)
Initialize:
- Priority queue:
[(0, 0)]
(distance 0 to itself) - Distances:
[0, inf, inf, inf]
- Minimum cost:
inf
Processing:
-
Pop
(0, 0)
- at city 0 with distance 0- Cost if buying here:
56 + 0*(2+1) = 56
- Update minimum:
ans = 56
- Explore neighbor city 1: update distance to 4, push
(4, 1)
- Cost if buying here:
-
Pop
(4, 1)
- at city 1 with distance 4- Cost if buying here:
42 + 4*3 = 54
- Update minimum:
ans = 54
- Explore neighbors:
- City 2: distance becomes 6, push
(6, 2)
- City 3: distance becomes 9, push
(9, 3)
- City 2: distance becomes 6, push
- Cost if buying here:
-
Pop
(6, 2)
- at city 2 with distance 6- Cost if buying here:
86 + 6*3 = 104
- Minimum stays:
ans = 54
- Explore city 3: distance becomes 7 (better than 9), push
(7, 3)
- Cost if buying here:
-
Pop
(7, 3)
- at city 3 with distance 7- Cost if buying here:
23 + 7*3 = 44
- Update minimum:
ans = 44
- Cost if buying here:
Result for starting from city 0: 44 (buy apple at city 3)
Step 3: Repeat for other starting cities
Starting from City 1 (originally City 2):
- Shortest paths: [4, 0, 2, 3]
- Costs: City 0:
56+12=68
, City 1:42+0=42
, City 2:86+6=92
, City 3:23+9=32
- Minimum: 32
Starting from City 2 (originally City 3):
- Shortest paths: [6, 2, 0, 1]
- Costs: City 0:
56+18=74
, City 1:42+6=48
, City 2:86+0=86
, City 3:23+3=26
- Minimum: 26
Starting from City 3 (originally City 4):
- Shortest paths: [7, 3, 1, 0]
- Costs: City 0:
56+21=77
, City 1:42+9=51
, City 2:86+3=89
, City 3:23+0=23
- Minimum: 23
Final Answer: [44, 32, 26, 23]
This means:
- Starting from city 1: minimum cost is 44 (go to city 4, buy apple for 23, return)
- Starting from city 2: minimum cost is 32 (go to city 4, buy apple for 23, return)
- Starting from city 3: minimum cost is 26 (go to city 4, buy apple for 23, return)
- Starting from city 4: minimum cost is 23 (buy apple locally, no travel needed)
Solution Implementation
1from heapq import heappush, heappop
2from collections import defaultdict
3from typing import List
4from math import inf
5
6class Solution:
7 def minCost(
8 self, n: int, roads: List[List[int]], appleCost: List[int], k: int
9 ) -> List[int]:
10 """
11 Find minimum cost to buy an apple starting from each city.
12
13 Args:
14 n: Number of cities (0-indexed)
15 roads: List of [city1, city2, cost] representing bidirectional roads (1-indexed)
16 appleCost: Cost of buying an apple in each city
17 k: Cost factor for traveling (total travel cost = road_cost * (k + 1))
18
19 Returns:
20 List where result[i] is minimum cost to buy an apple starting from city i
21 """
22
23 def dijkstra(start_city: int) -> int:
24 """
25 Use Dijkstra's algorithm to find minimum cost to buy an apple starting from start_city.
26
27 Args:
28 start_city: The starting city index (0-indexed)
29
30 Returns:
31 Minimum cost to buy an apple considering all reachable cities
32 """
33 # Priority queue: (distance_from_start, city)
34 priority_queue = [(0, start_city)]
35
36 # Track shortest distance from start_city to each city
37 shortest_distances = [inf] * n
38 shortest_distances[start_city] = 0
39
40 # Track minimum cost to buy an apple (including travel and purchase cost)
41 min_apple_cost = inf
42
43 while priority_queue:
44 current_distance, current_city = heappop(priority_queue)
45
46 # Calculate total cost: apple price + travel cost * (k + 1)
47 total_cost = appleCost[current_city] + current_distance * (k + 1)
48 min_apple_cost = min(min_apple_cost, total_cost)
49
50 # Explore neighbors
51 for neighbor_city, road_cost in adjacency_list[current_city]:
52 new_distance = shortest_distances[current_city] + road_cost
53
54 # If we found a shorter path to neighbor, update it
55 if shortest_distances[neighbor_city] > new_distance:
56 shortest_distances[neighbor_city] = new_distance
57 heappush(priority_queue, (new_distance, neighbor_city))
58
59 return min_apple_cost
60
61 # Build adjacency list for the graph (convert to 0-indexed)
62 adjacency_list = defaultdict(list)
63 for city1, city2, road_cost in roads:
64 # Convert from 1-indexed to 0-indexed
65 city1_idx = city1 - 1
66 city2_idx = city2 - 1
67
68 # Add bidirectional edges
69 adjacency_list[city1_idx].append((city2_idx, road_cost))
70 adjacency_list[city2_idx].append((city1_idx, road_cost))
71
72 # Calculate minimum cost for each starting city
73 result = [dijkstra(city) for city in range(n)]
74 return result
75
1class Solution {
2 // Constants and instance variables
3 private static final int INF = 0x3f3f3f3f; // Infinity value for initialization
4 private int travelCostMultiplier; // Multiplier for travel costs (k)
5 private int[] appleCosts; // Cost of apples at each city
6 private int[] distances; // Shortest distances from source
7 private List<int[]>[] adjacencyList; // Graph representation using adjacency list
8
9 /**
10 * Calculate minimum cost to buy apples from each city
11 * @param n Number of cities
12 * @param roads Array of roads where each road is [city1, city2, cost]
13 * @param appleCost Cost of apples in each city
14 * @param k Travel cost multiplier
15 * @return Array of minimum costs for buying apples starting from each city
16 */
17 public long[] minCost(int n, int[][] roads, int[] appleCost, int k) {
18 // Initialize instance variables
19 this.appleCosts = appleCost;
20 this.travelCostMultiplier = k;
21 this.distances = new int[n];
22 this.adjacencyList = new List[n];
23
24 // Initialize adjacency list for each city
25 for (int i = 0; i < n; ++i) {
26 adjacencyList[i] = new ArrayList<>();
27 }
28
29 // Build the undirected graph from roads
30 for (int[] road : roads) {
31 int cityA = road[0] - 1; // Convert to 0-indexed
32 int cityB = road[1] - 1; // Convert to 0-indexed
33 int roadCost = road[2];
34
35 // Add bidirectional edges
36 adjacencyList[cityA].add(new int[] {cityB, roadCost});
37 adjacencyList[cityB].add(new int[] {cityA, roadCost});
38 }
39
40 // Calculate minimum cost for each starting city
41 long[] result = new long[n];
42 for (int startCity = 0; startCity < n; ++startCity) {
43 result[startCity] = dijkstra(startCity);
44 }
45
46 return result;
47 }
48
49 /**
50 * Use Dijkstra's algorithm to find minimum cost to buy apples starting from given city
51 * @param startCity The starting city index
52 * @return Minimum total cost (travel + apple cost) from the starting city
53 */
54 private long dijkstra(int startCity) {
55 // Priority queue to store [distance, city] pairs, ordered by distance
56 PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((a, b) -> a[0] - b[0]);
57
58 // Initialize with starting city at distance 0
59 priorityQueue.offer(new int[] {0, startCity});
60
61 // Initialize all distances to infinity
62 Arrays.fill(distances, INF);
63 distances[startCity] = 0;
64
65 // Track minimum cost to buy apples
66 long minimumCost = Long.MAX_VALUE;
67
68 // Process cities in order of increasing distance
69 while (!priorityQueue.isEmpty()) {
70 int[] current = priorityQueue.poll();
71 int currentDistance = current[0];
72 int currentCity = current[1];
73
74 // Calculate total cost: apple cost + travel cost
75 // Travel cost = (k + 1) * distance (going and returning)
76 long totalCost = appleCosts[currentCity] + (long)(travelCostMultiplier + 1) * currentDistance;
77 minimumCost = Math.min(minimumCost, totalCost);
78
79 // Explore neighbors of current city
80 for (int[] neighbor : adjacencyList[currentCity]) {
81 int nextCity = neighbor[0];
82 int edgeWeight = neighbor[1];
83 int newDistance = distances[currentCity] + edgeWeight;
84
85 // Update distance if we found a shorter path
86 if (distances[nextCity] > newDistance) {
87 distances[nextCity] = newDistance;
88 priorityQueue.offer(new int[] {distances[nextCity], nextCity});
89 }
90 }
91 }
92
93 return minimumCost;
94 }
95}
96
1using ll = long long;
2using pii = pair<int, int>;
3
4class Solution {
5public:
6 const int INF = 0x3f3f3f3f;
7
8 vector<long long> minCost(int n, vector<vector<int>>& roads, vector<int>& appleCost, int k) {
9 // Build adjacency list representation of the graph
10 // Each city stores its connected cities with road costs
11 vector<vector<pii>> graph(n);
12 for (auto& road : roads) {
13 int cityA = road[0] - 1; // Convert to 0-indexed
14 int cityB = road[1] - 1; // Convert to 0-indexed
15 int roadCost = road[2];
16
17 // Add bidirectional edges
18 graph[cityA].push_back({cityB, roadCost});
19 graph[cityB].push_back({cityA, roadCost});
20 }
21
22 // Lambda function to run Dijkstra's algorithm from a starting city
23 // Returns minimum cost to buy an apple starting from the given city
24 auto dijkstra = [&](int startCity) -> long long {
25 // Initialize distances to all cities as infinity
26 int distances[n];
27 memset(distances, 63, sizeof(distances));
28
29 // Min-heap priority queue: {distance, city}
30 priority_queue<pii, vector<pii>, greater<pii>> minHeap;
31
32 // Start from the given city with distance 0
33 minHeap.push({0, startCity});
34 distances[startCity] = 0;
35
36 // Track minimum cost to buy an apple
37 // Cost = apple price + travel cost * (k + 1)
38 // where k is the cost multiplier per unit distance
39 ll minAppleCost = LONG_MAX;
40
41 // Process cities in order of increasing distance
42 while (!minHeap.empty()) {
43 auto [currentDist, currentCity] = minHeap.top();
44 minHeap.pop();
45
46 // Calculate total cost if we buy apple at current city
47 // Formula: apple cost + (travel distance * (k + 1))
48 ll totalCost = appleCost[currentCity] + 1LL * currentDist * (k + 1);
49 minAppleCost = min(minAppleCost, totalCost);
50
51 // Explore neighbors of current city
52 for (auto& [neighborCity, edgeWeight] : graph[currentCity]) {
53 int newDistance = distances[currentCity] + edgeWeight;
54
55 // If we found a shorter path to the neighbor, update it
56 if (distances[neighborCity] > newDistance) {
57 distances[neighborCity] = newDistance;
58 minHeap.push({newDistance, neighborCity});
59 }
60 }
61 }
62
63 return minAppleCost;
64 };
65
66 // Calculate minimum cost for each city as starting point
67 vector<ll> result(n);
68 for (int city = 0; city < n; ++city) {
69 result[city] = dijkstra(city);
70 }
71
72 return result;
73 }
74};
75
1type Long = number;
2type Pair = [number, number];
3
4const INF = 0x3f3f3f3f;
5
6function minCost(n: number, roads: number[][], appleCost: number[], k: number): number[] {
7 // Build adjacency list representation of the graph
8 // Each city stores its connected cities with road costs
9 const graph: Pair[][] = Array(n).fill(null).map(() => []);
10
11 for (const road of roads) {
12 const cityA = road[0] - 1; // Convert to 0-indexed
13 const cityB = road[1] - 1; // Convert to 0-indexed
14 const roadCost = road[2];
15
16 // Add bidirectional edges
17 graph[cityA].push([cityB, roadCost]);
18 graph[cityB].push([cityA, roadCost]);
19 }
20
21 // Function to run Dijkstra's algorithm from a starting city
22 // Returns minimum cost to buy an apple starting from the given city
23 const dijkstra = (startCity: number): Long => {
24 // Initialize distances to all cities as infinity
25 const distances: number[] = new Array(n).fill(INF);
26
27 // Min-heap priority queue: [distance, city]
28 // Using array with manual sorting to simulate priority queue
29 const minHeap: Pair[] = [];
30
31 // Helper function to add to heap and maintain min-heap property
32 const heapPush = (item: Pair): void => {
33 minHeap.push(item);
34 minHeap.sort((a, b) => a[0] - b[0]);
35 };
36
37 // Start from the given city with distance 0
38 heapPush([0, startCity]);
39 distances[startCity] = 0;
40
41 // Track minimum cost to buy an apple
42 // Cost = apple price + travel cost * (k + 1)
43 // where k is the cost multiplier per unit distance
44 let minAppleCost: Long = Number.MAX_SAFE_INTEGER;
45
46 // Process cities in order of increasing distance
47 while (minHeap.length > 0) {
48 const [currentDist, currentCity] = minHeap.shift()!;
49
50 // Skip if we've already processed this city with a shorter distance
51 if (currentDist > distances[currentCity]) {
52 continue;
53 }
54
55 // Calculate total cost if we buy apple at current city
56 // Formula: apple cost + (travel distance * (k + 1))
57 const totalCost: Long = appleCost[currentCity] + currentDist * (k + 1);
58 minAppleCost = Math.min(minAppleCost, totalCost);
59
60 // Explore neighbors of current city
61 for (const [neighborCity, edgeWeight] of graph[currentCity]) {
62 const newDistance = distances[currentCity] + edgeWeight;
63
64 // If we found a shorter path to the neighbor, update it
65 if (distances[neighborCity] > newDistance) {
66 distances[neighborCity] = newDistance;
67 heapPush([newDistance, neighborCity]);
68 }
69 }
70 }
71
72 return minAppleCost;
73 };
74
75 // Calculate minimum cost for each city as starting point
76 const result: Long[] = new Array(n);
77 for (let city = 0; city < n; city++) {
78 result[city] = dijkstra(city);
79 }
80
81 return result;
82}
83
Time and Space Complexity
Time Complexity: O(n × m × log m)
The algorithm runs Dijkstra's algorithm from each of the n
cities. For each Dijkstra execution:
- The while loop processes each edge at most once, giving us
O(m)
edge relaxations - Each edge relaxation involves a heap push operation, which costs
O(log |heap|)
- The heap can contain at most
m
elements (since we can push at most once per edge relaxation) - Therefore, each Dijkstra run takes
O(m × log m)
time
Since we run Dijkstra n
times (once for each starting city), the total time complexity is O(n × m × log m)
.
Space Complexity: O(n + m)
The space usage consists of:
- Graph adjacency list
g
: stores all edges, requiringO(m)
space (each edge is stored twice for undirected graph) - Distance array
dist
: requiresO(n)
space for each Dijkstra run - Priority queue
q
: can contain at mostO(m)
elements in the worst case - Result list: requires
O(n)
space
Since these data structures don't scale multiplicatively and the Dijkstra function reuses the same space for each iteration, the overall space complexity is O(n + m)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrect Distance Check in Dijkstra's Algorithm
The Problem: The current implementation has a subtle but critical bug in the Dijkstra's algorithm. When we pop a node from the priority queue, we don't verify if we've already processed this node with a shorter distance. This can lead to processing the same node multiple times with outdated distances, causing incorrect results.
# Current problematic code:
while priority_queue:
current_distance, current_city = heappop(priority_queue)
# No check if this is an outdated entry!
total_cost = appleCost[current_city] + current_distance * (k + 1)
min_apple_cost = min(min_apple_cost, total_cost)
Why This Happens: In Dijkstra's algorithm with a min-heap, we might push the same city multiple times with different distances. When we later pop an outdated entry (with a larger distance), we shouldn't process it again.
The Solution: Add a check to skip processing if the current distance is greater than the already recorded shortest distance:
while priority_queue:
current_distance, current_city = heappop(priority_queue)
# Skip if this is an outdated entry
if current_distance > shortest_distances[current_city]:
continue
total_cost = appleCost[current_city] + current_distance * (k + 1)
min_apple_cost = min(min_apple_cost, total_cost)
Pitfall 2: Integer Overflow with Large k Values
The Problem:
When calculating current_distance * (k + 1)
, if both current_distance
and k
are large, the multiplication might overflow in languages with fixed integer sizes. While Python handles arbitrary precision integers, this is a common issue when porting to other languages.
The Solution: Be mindful of the constraints and consider using appropriate data types or adding overflow checks:
# For very large values, consider checking bounds total_cost = appleCost[current_city] + current_distance * (k + 1) if total_cost > 10**15: # Or some reasonable upper bound continue # This path is too expensive anyway
Pitfall 3: Not Handling Edge Cases
The Problem: The code doesn't explicitly handle certain edge cases:
- What if a city has no roads connecting to it? (isolated city)
- What if
k = 0
? (no extra cost for return journey)
The Solution: The current implementation actually handles these cases correctly:
- Isolated cities will only consider buying an apple from themselves (distance = 0)
- When
k = 0
, the formulad * (k + 1) = d * 1 = d
correctly represents just the one-way cost
However, it's good practice to document or test these edge cases explicitly:
# Edge case: buying apple from starting city itself # This is automatically handled as shortest_distances[start_city] = 0 # Cost = appleCost[start_city] + 0 * (k + 1) = appleCost[start_city]
Complete Corrected Solution:
def dijkstra(start_city: int) -> int:
priority_queue = [(0, start_city)]
shortest_distances = [inf] * n
shortest_distances[start_city] = 0
min_apple_cost = inf
while priority_queue:
current_distance, current_city = heappop(priority_queue)
# Critical fix: Skip outdated entries
if current_distance > shortest_distances[current_city]:
continue
total_cost = appleCost[current_city] + current_distance * (k + 1)
min_apple_cost = min(min_apple_cost, total_cost)
for neighbor_city, road_cost in adjacency_list[current_city]:
new_distance = shortest_distances[current_city] + road_cost
if shortest_distances[neighbor_city] > new_distance:
shortest_distances[neighbor_city] = new_distance
heappush(priority_queue, (new_distance, neighbor_city))
return min_apple_cost
Which of the following shows the order of node visit in a Breadth-first Search?
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!