2463. Minimum Total Distance Traveled
Problem Description
You have robots and factories positioned along the X-axis. Each robot starts at a specific position given in the array robot[i]
, and each factory is located at factory[j][0]
with a repair capacity limit of factory[j][1]
robots.
All robots are initially broken and need to be repaired. You can choose the initial movement direction (left or right) for each robot. When a robot reaches a factory that hasn't reached its repair limit, the factory repairs the robot and it stops moving.
The goal is to assign directions to all robots such that the total distance traveled by all robots is minimized.
Key points to understand:
- Each robot and factory has a unique position on the X-axis
- A robot can initially be at the same position as a factory
- All robots move at the same speed
- Robots don't collide - they pass through each other
- If a factory has reached its repair limit, robots pass through it without stopping
- The distance traveled by a robot moving from position
x
to positiony
is|y - x|
The solution uses dynamic programming with memoization. After sorting both robots and factories by position, it defines dfs(i, j)
to find the minimum distance for assigning robots starting from index i
to factories starting from index j
.
For each factory j
, there are two choices:
- Skip this factory:
dfs(i, j) = dfs(i, j+1)
- Use this factory to repair
k
robots (wherek
ranges from 0 to the factory's limit): Calculate the total distance for thesek
robots to reach factoryj
, then adddfs(i+k+1, j+1)
for the remaining robots and factories
The algorithm returns the minimum among all possible assignments, ensuring every robot gets repaired while minimizing the total distance traveled.
Intuition
The key insight is that once we sort both robots and factories by position, the problem becomes an assignment problem where we want to match robots to factories optimally.
Why sorting helps: After sorting, we can observe that it's never optimal for robot assignments to "cross over". If robot A is to the left of robot B, and factory X is to the left of factory Y, it wouldn't make sense for robot A to go to factory Y while robot B goes to factory X - they would travel unnecessary extra distance.
This observation leads us to think about the problem sequentially: for each group of robots, we decide which factory they should go to. This naturally suggests a dynamic programming approach where we process robots and factories in order.
The state we need to track is: "Starting from robot i
and factory j
, what's the minimum distance to repair all remaining robots?" This is our dfs(i, j)
function.
For each factory, we face a decision: how many robots should this factory repair? Since factories have limits, we can't send all robots to one factory. We need to try different possibilities:
- Don't use this factory at all (skip to the next factory)
- Send 1 robot to this factory
- Send 2 robots to this factory
- ... up to the factory's limit
The greedy insight within this DP approach: if we decide factory j
will repair k
robots, it should repair the next k
consecutive robots starting from position i
. Why? Because these are the closest unassigned robots to this factory (remember, we sorted everything).
This combination of sorting + dynamic programming with memoization allows us to efficiently explore all valid assignments while avoiding redundant calculations, ultimately finding the optimal solution where the total distance is minimized.
Learn more about Dynamic Programming and Sorting patterns.
Solution Approach
The solution implements a top-down dynamic programming approach with memoization. Here's how the implementation works:
Step 1: Sorting
robot.sort() factory.sort()
First, we sort both the robots and factories arrays in ascending order by position. This ensures we can process them sequentially and avoid crossing assignments.
Step 2: Define the DP State
The function dfs(i, j)
represents the minimum total distance needed to repair all robots starting from index i
using factories starting from index j
.
Step 3: Base Cases
if i == len(robot):
return 0
if j == len(factory):
return inf
- If
i == len(robot)
, all robots have been assigned, so the distance is 0 - If
j == len(factory)
, we've run out of factories but still have robots to repair, which is impossible, so we return infinity
Step 4: Recursive Transitions
For each state (i, j)
, we have two main choices:
-
Skip the current factory:
ans = dfs(i, j + 1)
Move to the next factory without using the current one.
-
Use the current factory for k robots:
t = 0 for k in range(factory[j][1]): if i + k == len(robot): break t += abs(robot[i + k] - factory[j][0]) ans = min(ans, t + dfs(i + k + 1, j + 1))
- Try assigning 1, 2, ..., up to
limit
robots to factoryj
- For each choice of
k
robots, calculate the cumulative distancet
- The distance for robot at index
i+k
to reach factoryj
is|robot[i+k] - factory[j][0]|
- After assigning
k+1
robots to this factory, recursively solve for the remaining robots starting fromi+k+1
and the next factoryj+1
- Try assigning 1, 2, ..., up to
Step 5: Memoization
The @cache
decorator automatically memoizes the results of dfs(i, j)
, preventing redundant calculations for the same state.
Step 6: Return the Result
ans = dfs(0, 0) dfs.cache_clear() return ans
Start the recursion from the first robot and first factory, clear the cache to free memory, and return the minimum total distance.
The time complexity is O(m * n * limit)
where m
is the number of robots, n
is the number of factories, and limit
is the maximum capacity of any factory. The space complexity is O(m * n)
for the memoization cache.
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:
robot = [1, 6]
(2 robots at positions 1 and 6)factory = [[2, 1], [7, 1]]
(Factory at position 2 with capacity 1, Factory at position 7 with capacity 1)
Step 1: Sort arrays
robot = [1, 6]
(already sorted)factory = [[2, 1], [7, 1]]
(already sorted)
Step 2: Start DFS from dfs(0, 0)
We need to assign robots starting from index 0 to factories starting from index 0.
At dfs(0, 0): (Robot index 0, Factory index 0)
- Current robot at position 1, current factory at position 2 with capacity 1
- Option 1: Skip factory 0 →
dfs(0, 1)
- Option 2: Send 1 robot to factory 0
- Distance = |1 - 2| = 1
- Continue with
dfs(1, 1)
- Total for this option: 1 +
dfs(1, 1)
Let's explore Option 2 first:
At dfs(1, 1): (Robot index 1, Factory index 1)
- Current robot at position 6, current factory at position 7 with capacity 1
- Option 1: Skip factory 1 →
dfs(1, 2)
→ returns ∞ (no more factories) - Option 2: Send 1 robot to factory 1
- Distance = |6 - 7| = 1
- Continue with
dfs(2, 2)
→ returns 0 (all robots assigned) - Total for this option: 1 + 0 = 1
So dfs(1, 1) = 1
Back to dfs(0, 0):
- Option 2 gives us: 1 + 1 = 2
Now let's explore Option 1:
At dfs(0, 1): (Robot index 0, Factory index 1)
- Current robot at position 1, current factory at position 7 with capacity 1
- Option 1: Skip factory 1 →
dfs(0, 2)
→ returns ∞ (no more factories) - Option 2: Send 1 robot to factory 1
- Distance = |1 - 7| = 6
- Continue with
dfs(1, 2)
→ returns ∞ (robot at index 1 can't be assigned) - Total: ∞
So dfs(0, 1) = ∞
Final Result:
dfs(0, 0) = min(∞, 2) = 2
The optimal assignment is:
- Robot at position 1 → Factory at position 2 (distance = 1)
- Robot at position 6 → Factory at position 7 (distance = 1)
- Total distance = 2
This example shows how the algorithm explores different assignments, using memoization to avoid recalculating states, and ultimately finds the minimum total distance by trying all valid combinations of robot-to-factory assignments.
Solution Implementation
1class Solution:
2 def minimumTotalDistance(self, robot: List[int], factory: List[List[int]]) -> int:
3 """
4 Calculate the minimum total distance to repair all robots using available factories.
5
6 Args:
7 robot: List of robot positions
8 factory: List of [position, capacity] for each factory
9
10 Returns:
11 Minimum total distance for all robots to reach factories
12 """
13
14 @cache
15 def dp(robot_idx: int, factory_idx: int) -> int:
16 """
17 Dynamic programming function to find minimum distance.
18
19 Args:
20 robot_idx: Current index of robot being considered
21 factory_idx: Current index of factory being considered
22
23 Returns:
24 Minimum distance to repair robots from robot_idx onwards
25 using factories from factory_idx onwards
26 """
27 # Base case: All robots have been assigned
28 if robot_idx == len(robot):
29 return 0
30
31 # Base case: No more factories available but robots remain
32 if factory_idx == len(factory):
33 return float('inf')
34
35 # Option 1: Skip current factory
36 min_distance = dp(robot_idx, factory_idx + 1)
37
38 # Option 2: Use current factory for some robots
39 cumulative_distance = 0
40 factory_position = factory[factory_idx][0]
41 factory_capacity = factory[factory_idx][1]
42
43 # Try assigning 1 to capacity robots to current factory
44 for num_robots in range(factory_capacity):
45 # Check if we have enough robots remaining
46 if robot_idx + num_robots >= len(robot):
47 break
48
49 # Add distance for current robot to current factory
50 cumulative_distance += abs(robot[robot_idx + num_robots] - factory_position)
51
52 # Calculate total distance if we assign (num_robots + 1) robots to this factory
53 min_distance = min(
54 min_distance,
55 cumulative_distance + dp(robot_idx + num_robots + 1, factory_idx + 1)
56 )
57
58 return min_distance
59
60 # Sort both lists to enable optimal assignment
61 robot.sort()
62 factory.sort()
63
64 # Calculate the minimum total distance
65 result = dp(0, 0)
66
67 # Clear the cache to free memory
68 dp.cache_clear()
69
70 return result
71
1class Solution {
2 // Memoization table: dp[i][j] represents minimum distance for robots starting from index i
3 // and factories starting from index j
4 private long[][] dp;
5 private List<Integer> robotPositions;
6 private int[][] factoryData;
7
8 public long minimumTotalDistance(List<Integer> robot, int[][] factory) {
9 // Sort robots and factories by position for optimal assignment
10 Collections.sort(robot);
11 Arrays.sort(factory, (a, b) -> Integer.compare(a[0], b[0]));
12
13 // Initialize instance variables
14 this.robotPositions = robot;
15 this.factoryData = factory;
16 this.dp = new long[robot.size()][factory.length];
17
18 // Start DFS from first robot and first factory
19 return dfs(0, 0);
20 }
21
22 /**
23 * Recursively find minimum total distance to repair robots
24 * @param robotIndex current robot index to be assigned
25 * @param factoryIndex current factory index being considered
26 * @return minimum total distance for remaining robots
27 */
28 private long dfs(int robotIndex, int factoryIndex) {
29 // Base case: all robots have been assigned
30 if (robotIndex == robotPositions.size()) {
31 return 0;
32 }
33
34 // Base case: no more factories available but robots remain
35 if (factoryIndex == factoryData.length) {
36 return Long.MAX_VALUE / 1000; // Return large value to indicate invalid path
37 }
38
39 // Check memoization table
40 if (dp[robotIndex][factoryIndex] != 0) {
41 return dp[robotIndex][factoryIndex];
42 }
43
44 // Option 1: Skip current factory and try next one
45 long minDistance = dfs(robotIndex, factoryIndex + 1);
46
47 // Option 2: Assign robots to current factory
48 long currentDistance = 0;
49 int factoryCapacity = factoryData[factoryIndex][1];
50 int factoryPosition = factoryData[factoryIndex][0];
51
52 // Try assigning k robots to current factory (k from 1 to capacity)
53 for (int k = 0; k < factoryCapacity; k++) {
54 // Check if we have enough robots remaining
55 if (robotIndex + k >= robotPositions.size()) {
56 break;
57 }
58
59 // Add distance for current robot to factory
60 currentDistance += Math.abs(robotPositions.get(robotIndex + k) - factoryPosition);
61
62 // Recursively calculate minimum distance for remaining robots and factories
63 long remainingDistance = dfs(robotIndex + k + 1, factoryIndex + 1);
64
65 // Update minimum distance
66 minDistance = Math.min(minDistance, currentDistance + remainingDistance);
67 }
68
69 // Store result in memoization table
70 dp[robotIndex][factoryIndex] = minDistance;
71 return minDistance;
72 }
73}
74
1using ll = long long;
2
3class Solution {
4public:
5 long long minimumTotalDistance(vector<int>& robot, vector<vector<int>>& factory) {
6 // Sort robots and factories by position for optimal assignment
7 sort(robot.begin(), robot.end());
8 sort(factory.begin(), factory.end());
9
10 // dp[i][j] = minimum distance to repair robots from index i onwards using factories from index j onwards
11 vector<vector<ll>> dp(robot.size(), vector<ll>(factory.size()));
12
13 // DFS with memoization to find minimum total distance
14 function<ll(int, int)> dfs = [&](int robotIdx, int factoryIdx) -> ll {
15 // Base case: all robots have been assigned
16 if (robotIdx == robot.size()) {
17 return 0;
18 }
19
20 // Base case: no more factories available but robots remain
21 if (factoryIdx == factory.size()) {
22 return 1e15; // Return large value as impossible case
23 }
24
25 // Return memoized result if already computed
26 if (dp[robotIdx][factoryIdx]) {
27 return dp[robotIdx][factoryIdx];
28 }
29
30 // Option 1: Skip current factory and use remaining factories
31 ll minDistance = dfs(robotIdx, factoryIdx + 1);
32
33 // Option 2: Use current factory for some robots
34 ll accumulatedDistance = 0;
35 int factoryCapacity = factory[factoryIdx][1];
36 int factoryPosition = factory[factoryIdx][0];
37
38 // Try assigning k robots to current factory (k from 1 to capacity)
39 for (int k = 0; k < factoryCapacity; ++k) {
40 // Check if we have enough robots remaining
41 if (robotIdx + k >= robot.size()) {
42 break;
43 }
44
45 // Add distance for current robot to current factory
46 accumulatedDistance += abs(robot[robotIdx + k] - factoryPosition);
47
48 // Calculate total distance if we assign (k+1) robots to current factory
49 // and process remaining robots with remaining factories
50 minDistance = min(minDistance, accumulatedDistance + dfs(robotIdx + k + 1, factoryIdx + 1));
51 }
52
53 // Memoize and return the minimum distance
54 dp[robotIdx][factoryIdx] = minDistance;
55 return minDistance;
56 };
57
58 // Start DFS from first robot and first factory
59 return dfs(0, 0);
60 }
61};
62
1type ll = number;
2
3function minimumTotalDistance(robot: number[], factory: number[][]): number {
4 // Sort robots and factories by position for optimal assignment
5 robot.sort((a, b) => a - b);
6 factory.sort((a, b) => a[0] - b[0]);
7
8 // dp[i][j] = minimum distance to repair robots from index i onwards using factories from index j onwards
9 const dp: ll[][] = Array(robot.length).fill(null).map(() => Array(factory.length).fill(0));
10
11 // DFS with memoization to find minimum total distance
12 const dfs = (robotIdx: number, factoryIdx: number): ll => {
13 // Base case: all robots have been assigned
14 if (robotIdx === robot.length) {
15 return 0;
16 }
17
18 // Base case: no more factories available but robots remain
19 if (factoryIdx === factory.length) {
20 return 1e15; // Return large value as impossible case
21 }
22
23 // Return memoized result if already computed
24 if (dp[robotIdx][factoryIdx] !== 0) {
25 return dp[robotIdx][factoryIdx];
26 }
27
28 // Option 1: Skip current factory and use remaining factories
29 let minDistance: ll = dfs(robotIdx, factoryIdx + 1);
30
31 // Option 2: Use current factory for some robots
32 let accumulatedDistance: ll = 0;
33 const factoryCapacity: number = factory[factoryIdx][1];
34 const factoryPosition: number = factory[factoryIdx][0];
35
36 // Try assigning k robots to current factory (k from 1 to capacity)
37 for (let k = 0; k < factoryCapacity; k++) {
38 // Check if we have enough robots remaining
39 if (robotIdx + k >= robot.length) {
40 break;
41 }
42
43 // Add distance for current robot to current factory
44 accumulatedDistance += Math.abs(robot[robotIdx + k] - factoryPosition);
45
46 // Calculate total distance if we assign (k+1) robots to current factory
47 // and process remaining robots with remaining factories
48 minDistance = Math.min(minDistance, accumulatedDistance + dfs(robotIdx + k + 1, factoryIdx + 1));
49 }
50
51 // Memoize and return the minimum distance
52 dp[robotIdx][factoryIdx] = minDistance;
53 return minDistance;
54 };
55
56 // Start DFS from first robot and first factory
57 return dfs(0, 0);
58}
59
Time and Space Complexity
Time Complexity: O(m^2 × n)
The time complexity analysis breaks down as follows:
- The recursive function
dfs(i, j)
has states defined by parametersi
(robot index) andj
(factory index), creatingm × n
unique states wherem = len(robot)
andn = len(factory)
. - Due to memoization with
@cache
, each state is computed only once. - For each state
(i, j)
, the function iterates through up tofactory[j][1]
positions to assign robots to the current factory. In the worst case, this can be up tom
robots (when a factory has capacity equal to or greater than the number of robots). - Within this loop, each iteration performs
O(1)
operations (calculating distance and making recursive calls). - Therefore, the work done per state is
O(m)
, and withm × n
states, the total time complexity isO(m × n × m) = O(m^2 × n)
.
Space Complexity: O(m × n)
The space complexity consists of:
- The memoization cache stores up to
m × n
states (all possible combinations of robot and factory indices). - The recursion depth is at most
O(m + n)
in the worst case (processing all robots and factories). - Since
m × n
dominatesm + n
, the overall space complexity isO(m × n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Sort the Arrays
One of the most common mistakes is forgetting to sort both the robot
and factory
arrays before applying dynamic programming. Without sorting, the greedy assignment strategy breaks down because we might assign distant robots to nearby factories, leading to suboptimal solutions.
Why it matters: The DP solution relies on the fact that once sorted, we can assign robots to factories in order without worrying about crossing assignments (where a robot further left goes to a factory further right while a robot further right goes to a factory further left).
Solution:
# Always sort both arrays at the beginning robot.sort() factory.sort()
2. Off-by-One Error in the Loop
When iterating through the number of robots to assign to a factory, it's easy to make an off-by-one error:
Incorrect:
for num_robots in range(factory_capacity + 1): # Wrong! This tries to assign capacity+1 robots
if robot_idx + num_robots >= len(robot):
break
Correct:
for num_robots in range(factory_capacity): # Assigns 0 to capacity-1 robots (total of capacity iterations)
if robot_idx + num_robots >= len(robot):
break
# When using num_robots as index offset, we're actually assigning num_robots+1 robots
cumulative_distance += abs(robot[robot_idx + num_robots] - factory_position)
min_distance = min(min_distance,
cumulative_distance + dp(robot_idx + num_robots + 1, factory_idx + 1))
3. Not Handling the "Assign Zero Robots" Case
Some implementations forget that it's valid to not assign any robots to a factory (essentially skipping it). This is already handled by having two separate options in the code, but merging them incorrectly can cause issues:
Problematic approach:
# Starting from 1 instead of considering the skip option separately
for num_robots in range(1, factory_capacity + 1): # Misses the case of assigning 0 robots
...
Correct approach:
# Option 1: Skip factory (assign 0 robots)
min_distance = dp(robot_idx, factory_idx + 1)
# Option 2: Assign 1 or more robots
for num_robots in range(factory_capacity):
...
4. Integer Overflow with Large Values
When no valid assignment exists, returning a very large number is necessary. Using sys.maxsize
or a large integer might cause overflow issues in some languages or when adding distances.
Solution:
# Use float('inf') for impossible cases
if factory_idx == len(factory):
return float('inf')
5. Forgetting to Clear the Cache
While not affecting correctness, forgetting to clear the cache can cause memory issues in systems where the same Solution instance is reused for multiple test cases:
Solution:
result = dp(0, 0) dp.cache_clear() # Important for memory management return result
Which of the following array represent a max heap?
Recommended Readings
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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!