Prerequisites

Before attempting this problem, you should be comfortable with:

  • Sorting with Custom Comparators - Sorting objects by multiple criteria with tie-breaking rules
  • Manhattan Distance - Calculating distance as |x1-x2| + |y1-y2| on a 2D grid
  • Greedy Algorithms - Assigning resources optimally based on priority ordering
  • Bucket Sort - Using value ranges to group and process data without comparison sorting
  • Priority Queue / Heap - Efficiently retrieving minimum or maximum elements

1. Sorting

Intuition

We need to assign bikes to workers based on priority: smallest Manhattan distance first, then smallest worker index, then smallest bike index. By generating all possible worker-bike pairs with their distances and sorting them by these criteria, we can greedily assign each pair in order, skipping pairs where either the worker or bike is already taken.

Algorithm

  1. Calculate the Manhattan distance between every worker and every bike, creating triplets of (distance, worker index, bike index).
  2. Sort all triplets by distance first, then worker index, then bike index.
  3. Create arrays to track which bikes are taken and which workers have been assigned bikes.
  4. Iterate through the sorted triplets. For each pair where both the worker and bike are available, assign the bike to the worker.
  5. Stop once all workers have been assigned bikes. Return the assignment array.
class Solution:
    def assignBikes(self, workers: List[List[int]], bikes: List[List[int]]) -> List[int]:
        
        def find_distance(worker_loc, bike_loc):
            return abs(worker_loc[0] - bike_loc[0]) + abs(worker_loc[1] - bike_loc[1])
        
        # Calculate the distance between each worker and bike.
        all_triplets = []
        for worker, worker_loc in enumerate(workers):
            for bike, bike_loc in enumerate(bikes):
                distance = find_distance(worker_loc, bike_loc)
                all_triplets.append((distance, worker, bike))
        
        # Sort the triplets. By default, sorting will prioritize the
        # tuple's first value, then second value, and finally the third value
        all_triplets.sort()
        
        # Initialize all values to False, to signify no bikes have been taken
        bike_status = [False] * len(bikes)
        # Initialize all values to -1, to signify no worker has a bike
        worker_status = [-1] * len(workers)
        # Keep track of how many worker-bike pairs have been made
        pair_count = 0
        
        for distance, worker, bike in all_triplets:
            # If both worker and bike are free, assign the bike to
            # the worker and mark the bike as taken
            if worker_status[worker] == -1 and not bike_status[bike]:
                bike_status[bike] = True
                worker_status[worker] = bike
                pair_count += 1
                
                # If all the workers have the bike assigned, we can stop
                if pair_count == len(workers):
                    return worker_status
        
        return worker_status

Time & Space Complexity

  • Time complexity: O(NMlog(NM))O(N M \log (N M))
  • Space complexity: O(NM)O(N M)

Where NN is the number of workers, and MM is the number of bikes.


2. Bucket Sort

Intuition

Since Manhattan distances on a 1000x1000 grid are bounded (maximum 1998), we can use bucket sort instead of comparison-based sorting. By grouping worker-bike pairs by their distance, we can process pairs in distance order without explicit sorting. Within each distance bucket, pairs are naturally ordered by their insertion order (worker index first, then bike index).

Algorithm

  1. Calculate the Manhattan distance between every worker and every bike, storing pairs in a map keyed by distance.
  2. Track the minimum distance encountered.
  3. Starting from the minimum distance, iterate through distances in increasing order.
  4. For each distance, process all worker-bike pairs. If both the worker and bike are available, assign the bike to the worker.
  5. Continue until all workers have been assigned. Return the assignment array.
class Solution:
    def assignBikes(self, workers: List[List[int]], bikes: List[List[int]]) -> List[int]:
        
        def find_distance(worker_loc, bike_loc):
            return abs(worker_loc[0] - bike_loc[0]) + abs(worker_loc[1] - bike_loc[1])
        
        min_dist = float('inf')
        dist_to_pairs = collections.defaultdict(list)
        
        for worker, worker_loc in enumerate(workers):
            for bike, bike_loc in enumerate(bikes):
                distance = find_distance(worker_loc, bike_loc)
                dist_to_pairs[distance].append((worker, bike))
                min_dist = min(min_dist, distance)
                
        curr_dist = min_dist
        # Initialize all values to false, to signify no bikes have been taken
        bike_status = [False] * len(bikes)
        # Initialize all values to -1, to signify no worker has a bike
        worker_status = [-1] * len(workers)
        # Keep track of how many worker-bike pairs have been made
        pair_count = 0
        
        # While all workers have not been assigned a bike
        while pair_count < len(workers):
            for worker, bike in dist_to_pairs[curr_dist]:
                if worker_status[worker] == -1 and not bike_status[bike]:
                    # If both worker and bike are free, assign them to each other
                    bike_status[bike] = True
                    worker_status[worker] = bike
                    pair_count += 1
            curr_dist += 1
        
        return worker_status

Time & Space Complexity

  • Time complexity: O(NM+K)O(N M + K)
  • Space complexity: O(NM+K)O(N M + K)

Where NN is the number of workers, MM is the number of bikes, and KK is the maximum possible Manhattan distance of a worker/bike pair. In this problem, KK equals 19981998.


3. Priority Queue

Intuition

Instead of storing all worker-bike pairs upfront, we can use a priority queue that only tracks the current best option for each worker. When a bike is taken, we add the next best option for that worker to the queue. This reduces memory usage since we only need the closest available bike for each unassigned worker at any time.

Algorithm

  1. For each worker, sort their bike options by distance (and bike index as tiebreaker) in descending order, so we can pop the closest one efficiently.
  2. Add each worker's closest bike to a min-heap sorted by (distance, worker index, bike index).
  3. Pop the smallest element from the heap. If the bike is available, assign it to the worker.
  4. If the bike was taken, push the worker's next closest bike option onto the heap.
  5. Continue until all workers are assigned. Return the assignment array.
class Solution:
    def assignBikes(self, workers: List[List[int]], bikes: List[List[int]]) -> List[int]:
        
        def find_distance(worker_loc, bike_loc):
            return abs(worker_loc[0] - bike_loc[0]) + abs(worker_loc[1] - bike_loc[1])
        
        # List of triplets (distance, worker index, bike index) for each worker-bike combination
        worker_to_bike_list = []
        pq = []
        
        for worker, worker_loc in enumerate(workers):
            curr_worker_pairs = []
            for bike, bike_loc in enumerate(bikes):
                distance = find_distance(worker_loc, bike_loc)
                curr_worker_pairs.append((distance, worker, bike))
            
            # Sort the worker_to_bike_list for the current worker in reverse order
            curr_worker_pairs.sort(reverse=True)
            # Add the closest bike for this worker to the priority queue
            heapq.heappush(pq, curr_worker_pairs.pop())
            # Store the remaining options for the current worker in worker_to_bike_list
            worker_to_bike_list.append(curr_worker_pairs)
            
        # Initialize all values to false, to signify no bikes have been taken
        bike_status = [False] * len(bikes)
        # Initialize all values to -1, to signify no worker has a bike
        worker_status = [-1] * len(workers)
        
        while pq:
            # Pop the worker-bike pair with smallest distance
            distance, worker, bike = heapq.heappop(pq)
            
            if not bike_status[bike]:
                # If the bike is free, assign the bike to the worker
                bike_status[bike] = True
                worker_status[worker] = bike
            else:
                # Otherwise, add the next closest bike for the current worker to the priority queue
                next_closest_bike = worker_to_bike_list[worker].pop()
                heapq.heappush(pq, next_closest_bike)
        
        return worker_status

Time & Space Complexity

  • Time complexity: O(NMlogM)O(N M \log M)
  • Space complexity: O(NM)O(N M)

Where NN is the number of workers, and MM is the number of bikes.


Common Pitfalls

Incorrect Tie-Breaking Order

The problem requires breaking ties by worker index first, then bike index. Sorting by (distance, bike, worker) instead of (distance, worker, bike) produces wrong assignments.

# Wrong: bike index takes priority over worker index
all_triplets.sort(key=lambda x: (x[0], x[2], x[1]))

Using Euclidean Distance Instead of Manhattan Distance

The problem specifically asks for Manhattan distance. Using Euclidean distance will give incorrect results.

# Wrong: Euclidean distance
distance = math.sqrt((w[0] - b[0])**2 + (w[1] - b[1])**2)

Not Tracking Both Worker and Bike Availability

When a worker-bike pair is assigned, both must be marked as taken. Forgetting to check either condition leads to double assignments.