Facebook Pixel

2766. Relocate Marbles

MediumArrayHash TableSortingSimulation
Leetcode Link

Problem Description

You have an array nums that represents the initial positions of marbles. You need to perform a series of moves on these marbles using two arrays: moveFrom and moveTo (both of equal length).

For each step i from 0 to moveFrom.length - 1, you move all marbles currently at position moveFrom[i] to position moveTo[i].

After completing all moves, return a sorted list of all positions that contain at least one marble.

Key points to understand:

  • Multiple marbles can exist at the same position
  • When you move marbles from position moveFrom[i], you move ALL marbles at that position
  • A position is considered "occupied" if it has at least one marble
  • The final answer should be a sorted list of unique occupied positions

Example walkthrough: If nums = [1, 6, 7, 8], moveFrom = [1, 7, 2], and moveTo = [2, 9, 5]:

  • Initially, marbles are at positions: {1, 6, 7, 8}
  • Step 1: Move all marbles from position 1 to position 2 β†’ positions: {2, 6, 7, 8}
  • Step 2: Move all marbles from position 7 to position 9 β†’ positions: {2, 6, 8, 9}
  • Step 3: Move all marbles from position 2 to position 5 β†’ positions: {5, 6, 8, 9}
  • Return sorted: [5, 6, 8, 9]

The solution uses a set to track occupied positions. For each move operation, it removes the source position from the set and adds the destination position, effectively simulating the movement of all marbles from one position to another. Finally, it returns the sorted list of occupied positions.

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

Intuition

The key insight is that we don't need to track individual marbles or how many marbles are at each position. We only care about which positions are occupied after all moves are completed.

Think about what happens during a move operation: when we move all marbles from position moveFrom[i] to position moveTo[i], position moveFrom[i] becomes empty (unoccupied) and position moveTo[i] becomes occupied. If moveTo[i] was already occupied, it remains occupied with more marbles.

This leads us to realize that we can treat positions as either "occupied" or "unoccupied" - a binary state. A set data structure perfectly captures this, where presence in the set means the position is occupied.

The movement operation becomes simple:

  1. Remove the source position from our set (it's now empty)
  2. Add the destination position to our set (it's now occupied)

If the destination was already in the set, adding it again doesn't change anything (set property), which correctly models the behavior of adding more marbles to an already occupied position.

Why does this work? Because when we move ALL marbles from a position, that position definitely becomes empty. And the destination definitely becomes occupied, regardless of whether it was occupied before. The actual count of marbles doesn't matter for our final answer - we just need to know which positions have at least one marble.

This transforms a potentially complex marble-tracking problem into a simple set operation problem: start with initial positions, apply a series of remove/add operations, then return the sorted result.

Learn more about Sorting patterns.

Solution Approach

The solution uses a hash table (implemented as a Python set) to efficiently track occupied positions throughout the marble movements.

Step-by-step implementation:

  1. Initialize the position set: Create a set pos from the nums array. This gives us all initially occupied positions in O(n) time, where n is the length of nums.

    pos = set(nums)
  2. Process each move operation: Iterate through moveFrom and moveTo arrays simultaneously using zip(). For each pair (f, t):

    • Remove position f from the set (since all marbles at this position are moved away)
    • Add position t to the set (since marbles are moved here)
    for f, t in zip(moveFrom, moveTo):
        pos.remove(f)
        pos.add(t)
  3. Return sorted result: Convert the set to a sorted list and return it.

    return sorted(pos)

Why this works:

  • The remove(f) operation ensures position f is no longer marked as occupied after moving all marbles away from it
  • The add(t) operation ensures position t is marked as occupied after receiving marbles
  • If position t was already occupied, add(t) has no effect (set property), which is correct since the position remains occupied
  • The set automatically handles duplicates, giving us unique occupied positions

Time Complexity: O(n + m + k log k), where:

  • n = length of nums (initial set creation)
  • m = length of moveFrom/moveTo (processing moves)
  • k = number of final occupied positions (sorting)

Space Complexity: O(n) for storing the position set.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Given:

  • nums = [3, 3, 5] (initial marble positions)
  • moveFrom = [3, 5]
  • moveTo = [7, 6]

Step 1: Initialize the position set

pos = set([3, 3, 5]) = {3, 5}

Note that even though we have two marbles at position 3, the set only stores unique positions. We now have positions 3 and 5 marked as occupied.

Step 2: Process first move (moveFrom[0]=3, moveTo[0]=7)

  • Remove position 3 from the set: pos.remove(3) β†’ pos = {5}
  • Add position 7 to the set: pos.add(7) β†’ pos = {5, 7}

After this move, all marbles that were at position 3 (both of them) have moved to position 7. Position 3 is now empty.

Step 3: Process second move (moveFrom[1]=5, moveTo[1]=6)

  • Remove position 5 from the set: pos.remove(5) β†’ pos = {7}
  • Add position 6 to the set: pos.add(6) β†’ pos = {6, 7}

The marble at position 5 has moved to position 6. Position 5 is now empty.

Step 4: Return sorted result

return sorted(pos) = sorted({6, 7}) = [6, 7]

Final Answer: [6, 7]

This example demonstrates how the set-based approach elegantly handles:

  • Multiple marbles at the same position (two marbles at position 3)
  • Tracking only occupied positions without counting individual marbles
  • Efficient position updates through simple set operations

Solution Implementation

1class Solution:
2    def relocateMarbles(
3        self, nums: List[int], moveFrom: List[int], moveTo: List[int]
4    ) -> List[int]:
5        # Initialize a set to track current marble positions
6        # Using a set automatically handles duplicates and provides O(1) operations
7        marble_positions = set(nums)
8      
9        # Process each move operation
10        # Each move transfers all marbles from one position to another
11        for from_position, to_position in zip(moveFrom, moveTo):
12            # Remove all marbles from the source position
13            marble_positions.remove(from_position)
14            # Add all marbles to the destination position
15            # If position already exists, set automatically handles it
16            marble_positions.add(to_position)
17      
18        # Return sorted list of unique positions where marbles exist
19        return sorted(marble_positions)
20
1class Solution {
2    /**
3     * Relocates marbles from their initial positions based on a series of moves.
4     * Each move transfers all marbles from one position to another.
5     * 
6     * @param nums Initial positions of marbles (may contain duplicates)
7     * @param moveFrom Array of source positions for each move
8     * @param moveTo Array of destination positions for each move
9     * @return Sorted list of unique positions where marbles exist after all moves
10     */
11    public List<Integer> relocateMarbles(int[] nums, int[] moveFrom, int[] moveTo) {
12        // Use HashSet to track unique marble positions
13        Set<Integer> marblePositions = new HashSet<>();
14      
15        // Initialize set with all initial marble positions
16        for (int position : nums) {
17            marblePositions.add(position);
18        }
19      
20        // Process each move sequentially
21        for (int i = 0; i < moveFrom.length; i++) {
22            // Remove all marbles from the source position
23            marblePositions.remove(moveFrom[i]);
24            // Add marbles to the destination position
25            marblePositions.add(moveTo[i]);
26        }
27      
28        // Convert set to list for sorting
29        List<Integer> result = new ArrayList<>(marblePositions);
30      
31        // Sort positions in ascending order
32        result.sort((a, b) -> a - b);
33      
34        return result;
35    }
36}
37
1class Solution {
2public:
3    vector<int> relocateMarbles(vector<int>& nums, vector<int>& moveFrom, vector<int>& moveTo) {
4        // Create a set to track unique marble positions
5        // Initially contains all positions from the input array
6        unordered_set<int> marblePositions(nums.begin(), nums.end());
7      
8        // Process each move operation
9        for (int i = 0; i < moveFrom.size(); ++i) {
10            // Remove all marbles from the source position
11            marblePositions.erase(moveFrom[i]);
12          
13            // Add marbles to the destination position
14            // If position already exists, set automatically handles duplicates
15            marblePositions.insert(moveTo[i]);
16        }
17      
18        // Convert the set of positions to a vector
19        vector<int> result(marblePositions.begin(), marblePositions.end());
20      
21        // Sort the positions in ascending order
22        sort(result.begin(), result.end());
23      
24        return result;
25    }
26};
27
1/**
2 * Relocates marbles from one position to another and returns sorted occupied positions
3 * @param nums - Initial positions of marbles
4 * @param moveFrom - Array of positions to move marbles from
5 * @param moveTo - Array of positions to move marbles to
6 * @returns Sorted array of final occupied positions
7 */
8function relocateMarbles(nums: number[], moveFrom: number[], moveTo: number[]): number[] {
9    // Create a set to track occupied positions (removes duplicates automatically)
10    const occupiedPositions: Set<number> = new Set<number>(nums);
11  
12    // Process each move operation
13    for (let i = 0; i < moveFrom.length; i++) {
14        // Remove marble from source position
15        occupiedPositions.delete(moveFrom[i]);
16        // Add marble to destination position
17        occupiedPositions.add(moveTo[i]);
18    }
19  
20    // Convert set to array and sort in ascending order
21    const sortedPositions: number[] = Array.from(occupiedPositions).sort((a: number, b: number) => a - b);
22  
23    return sortedPositions;
24}
25

Time and Space Complexity

Time Complexity: O(n Γ— log n + m) where n is the length of array nums and m is the length of moveFrom/moveTo arrays.

  • Creating the initial set from nums: O(n)
  • The loop iterates m times, and each iteration:
    • remove(f): O(1) average case for set removal
    • add(t): O(1) average case for set addition
  • Sorting the final set: O(k Γ— log k) where k is the size of the final set, and k ≀ n + m
  • The dominant operation is sorting, which in the worst case is O((n + m) Γ— log(n + m))
  • However, if we consider m as proportional to or less than n, the complexity simplifies to O(n Γ— log n)

Space Complexity: O(n)

  • The set pos stores at most n + m unique positions, but typically stores around n elements
  • The sorted list returned also contains at most n elements
  • If we consider m as proportional to or less than n, the space complexity is O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. KeyError when removing non-existent positions

The most critical pitfall in this solution is assuming that moveFrom[i] always exists in the current set of positions. If a position in moveFrom doesn't currently have any marbles, calling remove() will raise a KeyError.

Problem scenario:

nums = [1, 2, 3]
moveFrom = [1, 4]  # Position 4 doesn't have any marbles!
moveTo = [5, 6]

# This will crash with KeyError when trying to remove position 4
marble_positions.remove(4)  # KeyError: 4

Solution: Use discard() instead of remove(), or check existence before removing:

# Option 1: Using discard() - doesn't raise error if element doesn't exist
for from_position, to_position in zip(moveFrom, moveTo):
    marble_positions.discard(from_position)  # Safe removal
    marble_positions.add(to_position)

# Option 2: Check before removing
for from_position, to_position in zip(moveFrom, moveTo):
    if from_position in marble_positions:
        marble_positions.remove(from_position)
    marble_positions.add(to_position)

2. Assuming moveFrom and moveTo have no overlaps within the same step

While the current solution handles this correctly, developers might overthink the scenario where moveFrom[i] == moveTo[i] (moving marbles to the same position). The solution works because:

  • remove() happens first, removing the position
  • add() happens second, adding it back
  • Net effect: position remains occupied (correct behavior)

3. Not handling the case where destination already has marbles

Some might incorrectly think they need to track marble counts at each position. The problem only asks for positions with at least one marble, not the count. Using a set correctly abstracts this - adding to an existing position has no effect, which is exactly what we want.

Incorrect approach (unnecessary complexity):

# Unnecessarily tracking counts
marble_counts = {}
for pos in nums:
    marble_counts[pos] = marble_counts.get(pos, 0) + 1
# ... complex logic to update counts during moves

The set-based approach elegantly avoids this complexity by focusing only on position occupancy, not marble counts.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the maximum element can be found in:


Recommended Readings

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

Load More