Facebook Pixel

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 position y 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:

  1. Skip this factory: dfs(i, j) = dfs(i, j+1)
  2. Use this factory to repair k robots (where k ranges from 0 to the factory's limit): Calculate the total distance for these k robots to reach factory j, then add dfs(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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Skip the current factory:

    ans = dfs(i, j + 1)

    Move to the next factory without using the current one.

  2. 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 factory j
    • For each choice of k robots, calculate the cumulative distance t
    • The distance for robot at index i+k to reach factory j is |robot[i+k] - factory[j][0]|
    • After assigning k+1 robots to this factory, recursively solve for the remaining robots starting from i+k+1 and the next factory j+1

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 Evaluator

Example 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 parameters i (robot index) and j (factory index), creating m × n unique states where m = len(robot) and n = len(factory).
  • Due to memoization with @cache, each state is computed only once.
  • For each state (i, j), the function iterates through up to factory[j][1] positions to assign robots to the current factory. In the worst case, this can be up to m 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 with m × n states, the total time complexity is O(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 dominates m + n, the overall space complexity is O(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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More