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
- Calculate the Manhattan distance between every worker and every bike, creating triplets of (distance, worker index, bike index).
- Sort all triplets by distance first, then worker index, then bike index.
- Create arrays to track which bikes are taken and which workers have been assigned bikes.
- Iterate through the sorted triplets. For each pair where both the worker and bike are available, assign the bike to the worker.
- 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_statusTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of workers, and 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
- Calculate the Manhattan distance between every worker and every bike, storing pairs in a map keyed by distance.
- Track the minimum distance encountered.
- Starting from the minimum distance, iterate through distances in increasing order.
- For each distance, process all worker-bike pairs. If both the worker and bike are available, assign the bike to the worker.
- 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_statusTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of workers, is the number of bikes, and is the maximum possible Manhattan distance of a worker/bike pair. In this problem, equals .
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
- 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.
- Add each worker's closest bike to a min-heap sorted by (distance, worker index, bike index).
- Pop the smallest element from the heap. If the bike is available, assign it to the worker.
- If the bike was taken, push the worker's next closest bike option onto the heap.
- 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_statusTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of workers, and 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.