1057. Campus Bikes 🔒
Problem Description
You have n
workers and m
bikes positioned on a 2D coordinate plane (where n ≤ m
). Each worker needs to be assigned exactly one bike.
Input:
workers[i] = [xi, yi]
represents the position of thei-th
workerbikes[j] = [xj, yj]
represents the position of thej-th
bike- All positions are unique
Assignment Rules:
- Calculate the Manhattan distance between each worker-bike pair:
|worker.x - bike.x| + |worker.y - bike.y|
- Assign bikes using these priorities:
- Choose the worker-bike pair with the shortest Manhattan distance
- If multiple pairs have the same distance, choose the pair with the smallest worker index
- If still tied, choose the pair with the smallest bike index
- Once a worker is assigned a bike, both become unavailable for future assignments
- Repeat until all workers have bikes
Output:
Return an array answer
where answer[i]
is the index of the bike assigned to worker i
.
Example:
If you have workers at positions [[0,0], [2,1]]
and bikes at [[1,2], [3,3]]
:
- Worker 0 to Bike 0: distance =
|0-1| + |0-2| = 3
- Worker 0 to Bike 1: distance =
|0-3| + |0-3| = 6
- Worker 1 to Bike 0: distance =
|2-1| + |1-2| = 2
- Worker 1 to Bike 1: distance =
|2-3| + |1-3| = 3
The smallest distance is 2 (Worker 1 to Bike 0), so Worker 1 gets Bike 0. Then Worker 0 gets the remaining Bike 1. Result: [1, 0]
.
Intuition
The key insight is that this is a greedy assignment problem where we need to make optimal local choices based on a specific priority order. Since we want to assign bikes to workers following strict rules (shortest distance first, then worker index, then bike index), we can think of this as processing all possible worker-bike pairs in a sorted order.
The natural approach is to:
- Generate all possible pairings: Since we need to consider every worker with every bike, we create all
n × m
possible combinations - Sort by priority: Instead of repeatedly searching for the minimum distance pair, we can sort all pairs once according to our criteria
- Greedily assign: Process pairs in sorted order and make assignments when both the worker and bike are still available
Why does greedy work here? Because once we've sorted all pairs by (distance, worker_index, bike_index)
, the first valid pair we encounter for any worker will always be their optimal assignment according to the problem's rules. There's no benefit in "saving" a bike for a different worker since we're already processing in the globally optimal order.
The sorting naturally handles all tiebreakers:
- Python's sort is stable and compares tuples element by element
- A tuple
(distance, worker_id, bike_id)
will automatically sort by distance first, then worker index, then bike index
This transforms a potentially complex matching problem into a simple: sort everything, then iterate and assign. The time complexity is dominated by sorting O(nm log(nm))
, which is acceptable given the problem constraints.
Learn more about Sorting and Heap (Priority Queue) patterns.
Solution Approach
The implementation follows a straightforward greedy approach with sorting:
Step 1: Generate All Pairs with Distances
arr = []
for i, j in product(range(n), range(m)):
dist = abs(workers[i][0] - bikes[j][0]) + abs(workers[i][1] - bikes[j][1])
arr.append((dist, i, j))
- Use
itertools.product
to generate all(worker_index, bike_index)
combinations - Calculate Manhattan distance for each pair:
|x1 - x2| + |y1 - y2|
- Store each combination as a tuple
(distance, worker_index, bike_index)
Step 2: Sort the Pairs
arr.sort()
- Python sorts tuples lexicographically (element by element)
- This automatically gives us the correct priority order:
- Primary: shortest distance
- Secondary: smallest worker index
- Tertiary: smallest bike index
Step 3: Track Assignments
vis1 = [False] * n # Track assigned workers vis2 = [False] * m # Track assigned bikes ans = [0] * n # Store bike assignment for each worker
- Use boolean arrays to mark workers and bikes as "used"
- Initialize result array to store final assignments
Step 4: Greedy Assignment
for _, i, j in arr: if not vis1[i] and not vis2[j]: vis1[i] = vis2[j] = True ans[i] = j
- Iterate through sorted pairs in order
- For each pair
(distance, worker_i, bike_j)
:- Check if both worker
i
and bikej
are available - If yes, make the assignment: worker
i
gets bikej
- Mark both as used to prevent reassignment
- Check if both worker
- Skip pairs where either the worker or bike is already assigned
Time Complexity: O(nm log(nm))
for generating and sorting all pairs
Space Complexity: O(nm)
for storing all pairs
The algorithm guarantees optimal assignments because we process pairs in the exact priority order specified by the problem. Once a worker is assigned, we never need to reconsider that assignment since we've already evaluated all better options.
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 with 2 workers and 3 bikes:
- Workers:
[[0,0], [1,1]]
- Bikes:
[[0,1], [4,2], [2,1]]
Step 1: Calculate all worker-bike distances
Worker | Bike | Distance Calculation | Distance | Triple |
---|---|---|---|---|
0 | 0 | |0-0| + |0-1| = 0+1 | 1 | (1, 0, 0) |
0 | 1 | |0-4| + |0-2| = 4+2 | 6 | (6, 0, 1) |
0 | 2 | |0-2| + |0-1| = 2+1 | 3 | (3, 0, 2) |
1 | 0 | |1-0| + |1-1| = 1+0 | 1 | (1, 1, 0) |
1 | 1 | |1-4| + |1-2| = 3+1 | 4 | (4, 1, 1) |
1 | 2 | |1-2| + |1-1| = 1+0 | 1 | (1, 1, 2) |
Step 2: Sort all pairs by (distance, worker_index, bike_index)
Sorted list:
(1, 0, 0)
- Worker 0 to Bike 0, distance 1(1, 1, 0)
- Worker 1 to Bike 0, distance 1 (tied distance, worker 1 > 0)(1, 1, 2)
- Worker 1 to Bike 2, distance 1 (tied distance & worker)(3, 0, 2)
- Worker 0 to Bike 2, distance 3(4, 1, 1)
- Worker 1 to Bike 1, distance 4(6, 0, 1)
- Worker 0 to Bike 1, distance 6
Step 3: Process pairs greedily
Initialize:
vis1 = [False, False]
(workers availability)vis2 = [False, False, False]
(bikes availability)ans = [_, _]
(assignments)
Processing:
-
(1, 0, 0)
: Worker 0 and Bike 0 both available- Assign: Worker 0 gets Bike 0
- Update:
vis1 = [True, False]
,vis2 = [True, False, False]
,ans = [0, _]
-
(1, 1, 0)
: Worker 1 available but Bike 0 already taken- Skip
-
(1, 1, 2)
: Worker 1 and Bike 2 both available- Assign: Worker 1 gets Bike 2
- Update:
vis1 = [True, True]
,vis2 = [True, False, True]
,ans = [0, 2]
-
All workers assigned, stop processing
Final Result: [0, 2]
- Worker 0 is assigned Bike 0
- Worker 1 is assigned Bike 2
Solution Implementation
1from typing import List
2from itertools import product
3
4class Solution:
5 def assignBikes(
6 self, workers: List[List[int]], bikes: List[List[int]]
7 ) -> List[int]:
8 """
9 Assign bikes to workers based on shortest Manhattan distance.
10 Each worker gets exactly one bike and each bike can be assigned to only one worker.
11
12 Args:
13 workers: List of worker positions [x, y]
14 bikes: List of bike positions [x, y]
15
16 Returns:
17 List where index i contains the bike index assigned to worker i
18 """
19 num_workers = len(workers)
20 num_bikes = len(bikes)
21
22 # Create list of (distance, worker_index, bike_index) tuples
23 distance_pairs = []
24 for worker_idx, bike_idx in product(range(num_workers), range(num_bikes)):
25 # Calculate Manhattan distance between worker and bike
26 manhattan_distance = (
27 abs(workers[worker_idx][0] - bikes[bike_idx][0]) +
28 abs(workers[worker_idx][1] - bikes[bike_idx][1])
29 )
30 distance_pairs.append((manhattan_distance, worker_idx, bike_idx))
31
32 # Sort by distance (ties broken by worker index, then bike index due to tuple comparison)
33 distance_pairs.sort()
34
35 # Track which workers and bikes have been assigned
36 worker_assigned = [False] * num_workers
37 bike_assigned = [False] * num_bikes
38
39 # Result array: index i stores the bike assigned to worker i
40 result = [0] * num_workers
41
42 # Greedily assign bikes to workers based on sorted distances
43 for distance, worker_idx, bike_idx in distance_pairs:
44 # If both worker and bike are unassigned, make the assignment
45 if not worker_assigned[worker_idx] and not bike_assigned[bike_idx]:
46 worker_assigned[worker_idx] = True
47 bike_assigned[bike_idx] = True
48 result[worker_idx] = bike_idx
49
50 return result
51
1class Solution {
2 public int[] assignBikes(int[][] workers, int[][] bikes) {
3 int numWorkers = workers.length;
4 int numBikes = bikes.length;
5
6 // Create array to store all possible worker-bike pairs with their distances
7 // Each element contains [distance, workerIndex, bikeIndex]
8 int[][] workerBikePairs = new int[numWorkers * numBikes][3];
9
10 // Generate all possible worker-bike combinations
11 int pairIndex = 0;
12 for (int workerIdx = 0; workerIdx < numWorkers; workerIdx++) {
13 for (int bikeIdx = 0; bikeIdx < numBikes; bikeIdx++) {
14 // Calculate Manhattan distance between worker and bike
15 int distance = Math.abs(workers[workerIdx][0] - bikes[bikeIdx][0]) +
16 Math.abs(workers[workerIdx][1] - bikes[bikeIdx][1]);
17
18 // Store distance, worker index, and bike index
19 workerBikePairs[pairIndex++] = new int[] {distance, workerIdx, bikeIdx};
20 }
21 }
22
23 // Sort pairs by: 1) distance (ascending), 2) worker index (ascending), 3) bike index (ascending)
24 Arrays.sort(workerBikePairs, (pair1, pair2) -> {
25 // First, compare by distance
26 if (pair1[0] != pair2[0]) {
27 return pair1[0] - pair2[0];
28 }
29 // If distances are equal, compare by worker index
30 if (pair1[1] != pair2[1]) {
31 return pair1[1] - pair2[1];
32 }
33 // If worker indices are also equal, compare by bike index
34 return pair1[2] - pair2[2];
35 });
36
37 // Track which workers and bikes have been assigned
38 boolean[] assignedWorkers = new boolean[numWorkers];
39 boolean[] assignedBikes = new boolean[numBikes];
40
41 // Result array where result[i] = bike index assigned to worker i
42 int[] result = new int[numWorkers];
43
44 // Iterate through sorted pairs and assign bikes to workers
45 for (int[] pair : workerBikePairs) {
46 int workerIdx = pair[1];
47 int bikeIdx = pair[2];
48
49 // If both worker and bike are unassigned, make the assignment
50 if (!assignedWorkers[workerIdx] && !assignedBikes[bikeIdx]) {
51 assignedWorkers[workerIdx] = true;
52 assignedBikes[bikeIdx] = true;
53 result[workerIdx] = bikeIdx;
54 }
55 }
56
57 return result;
58 }
59}
60
1class Solution {
2public:
3 vector<int> assignBikes(vector<vector<int>>& workers, vector<vector<int>>& bikes) {
4 int numWorkers = workers.size();
5 int numBikes = bikes.size();
6
7 // Store all possible (distance, workerIndex, bikeIndex) combinations
8 vector<tuple<int, int, int>> distancePairs;
9 distancePairs.reserve(numWorkers * numBikes);
10
11 // Calculate Manhattan distance for each worker-bike pair
12 for (int workerIdx = 0; workerIdx < numWorkers; ++workerIdx) {
13 for (int bikeIdx = 0; bikeIdx < numBikes; ++bikeIdx) {
14 int manhattanDistance = abs(workers[workerIdx][0] - bikes[bikeIdx][0]) +
15 abs(workers[workerIdx][1] - bikes[bikeIdx][1]);
16 distancePairs.push_back({manhattanDistance, workerIdx, bikeIdx});
17 }
18 }
19
20 // Sort by distance (and implicitly by worker index, then bike index for ties)
21 sort(distancePairs.begin(), distancePairs.end());
22
23 // Track which workers and bikes have been assigned
24 vector<bool> workerAssigned(numWorkers, false);
25 vector<bool> bikeAssigned(numBikes, false);
26
27 // Result array where result[i] = bike index assigned to worker i
28 vector<int> result(numWorkers);
29
30 // Greedily assign bikes to workers based on sorted distances
31 for (const auto& [distance, workerIdx, bikeIdx] : distancePairs) {
32 // If both worker and bike are unassigned, make the assignment
33 if (!workerAssigned[workerIdx] && !bikeAssigned[bikeIdx]) {
34 workerAssigned[workerIdx] = true;
35 bikeAssigned[bikeIdx] = true;
36 result[workerIdx] = bikeIdx;
37 }
38 }
39
40 return result;
41 }
42};
43
1function assignBikes(workers: number[][], bikes: number[][]): number[] {
2 const numWorkers = workers.length;
3 const numBikes = bikes.length;
4
5 // Store all possible [distance, workerIndex, bikeIndex] combinations
6 const distancePairs: [number, number, number][] = [];
7
8 // Calculate Manhattan distance for each worker-bike pair
9 for (let workerIdx = 0; workerIdx < numWorkers; workerIdx++) {
10 for (let bikeIdx = 0; bikeIdx < numBikes; bikeIdx++) {
11 // Manhattan distance = |x1 - x2| + |y1 - y2|
12 const manhattanDistance = Math.abs(workers[workerIdx][0] - bikes[bikeIdx][0]) +
13 Math.abs(workers[workerIdx][1] - bikes[bikeIdx][1]);
14 distancePairs.push([manhattanDistance, workerIdx, bikeIdx]);
15 }
16 }
17
18 // Sort by distance first, then by worker index, then by bike index for ties
19 distancePairs.sort((a, b) => {
20 // Compare by distance
21 if (a[0] !== b[0]) return a[0] - b[0];
22 // If distance is same, compare by worker index
23 if (a[1] !== b[1]) return a[1] - b[1];
24 // If worker index is also same, compare by bike index
25 return a[2] - b[2];
26 });
27
28 // Track which workers and bikes have been assigned
29 const workerAssigned: boolean[] = new Array(numWorkers).fill(false);
30 const bikeAssigned: boolean[] = new Array(numBikes).fill(false);
31
32 // Result array where result[i] = bike index assigned to worker i
33 const result: number[] = new Array(numWorkers);
34
35 // Greedily assign bikes to workers based on sorted distances
36 for (const [distance, workerIdx, bikeIdx] of distancePairs) {
37 // If both worker and bike are unassigned, make the assignment
38 if (!workerAssigned[workerIdx] && !bikeAssigned[bikeIdx]) {
39 workerAssigned[workerIdx] = true;
40 bikeAssigned[bikeIdx] = true;
41 result[workerIdx] = bikeIdx;
42 }
43 }
44
45 return result;
46}
47
Time and Space Complexity
Time Complexity: O(n * m * log(n * m))
- Creating the array
arr
: We iterate through all possible combinations of workers and bikes usingproduct(range(n), range(m))
, which generatesn * m
pairs. For each pair, we calculate the Manhattan distance and append to the array. This takesO(n * m)
time. - Sorting the array: The
arr.sort()
operation sortsn * m
elements, which takesO(n * m * log(n * m))
time. - Iterating through the sorted array: We iterate through all
n * m
elements once to assign bikes to workers. Each iteration involves constant time operations (checking and updating boolean arrays, assigning values). This takesO(n * m)
time. - Overall time complexity is dominated by the sorting step:
O(n * m * log(n * m))
Space Complexity: O(n * m)
- The array
arr
storesn * m
tuples, where each tuple contains 3 integers (distance, worker index, bike index). This requiresO(n * m)
space. - Two boolean arrays
vis1
andvis2
of sizesn
andm
respectively, requiringO(n + m)
space. - The answer array
ans
of sizen
, requiringO(n)
space. - Overall space complexity is dominated by the
arr
array:O(n * m)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Attempting Priority Queue/Heap Approach
A common mistake is trying to use a min-heap to avoid sorting all pairs upfront:
Incorrect Approach:
import heapq
def assignBikes_wrong(workers, bikes):
heap = []
for i in range(len(workers)):
for j in range(len(bikes)):
dist = abs(workers[i][0] - bikes[j][0]) + abs(workers[i][1] - bikes[j][1])
heapq.heappush(heap, (dist, i, j))
result = [0] * len(workers)
worker_assigned = [False] * len(workers)
bike_assigned = [False] * len(bikes)
while heap:
dist, w, b = heapq.heappop(heap)
if not worker_assigned[w] and not bike_assigned[b]:
worker_assigned[w] = bike_assigned[b] = True
result[w] = b
return result
Why This Fails: While this looks more efficient, it produces the same O(nm log(nm)) complexity since we still push all pairs to the heap. The heap doesn't provide any advantage here because we need to consider all pairs in sorted order anyway.
2. Misunderstanding Assignment Priority
Developers often incorrectly assume they should find the best bike for each worker independently:
Incorrect Approach:
def assignBikes_wrong(workers, bikes):
result = []
used_bikes = set()
for i, worker in enumerate(workers):
best_bike = -1
best_dist = float('inf')
for j, bike in enumerate(bikes):
if j not in used_bikes:
dist = abs(worker[0] - bike[0]) + abs(worker[1] - bike[1])
if dist < best_dist:
best_dist = dist
best_bike = j
result.append(best_bike)
used_bikes.add(best_bike)
return result
Why This Fails: This assigns bikes worker by worker, but the problem requires global optimization. A worker with a lower index might need to wait for a farther bike if another worker has a closer distance to their optimal bike.
3. Distance Constraint Optimization (Bucket Sort)
Some try to optimize using the bounded distance property (maximum Manhattan distance ≤ 2000):
Overengineered Approach:
def assignBikes_bucket(workers, bikes):
# Bucket sort approach - technically correct but overcomplicated
buckets = [[] for _ in range(2001)] # Max possible distance
for i in range(len(workers)):
for j in range(len(bikes)):
dist = abs(workers[i][0] - bikes[j][0]) + abs(workers[i][1] - bikes[j][1])
buckets[dist].append((i, j))
# Sort within each bucket by worker index, then bike index
for bucket in buckets:
bucket.sort()
# Process buckets in order
result = [0] * len(workers)
worker_assigned = [False] * len(workers)
bike_assigned = [False] * len(bikes)
for dist in range(2001):
for w, b in buckets[dist]:
if not worker_assigned[w] and not bike_assigned[b]:
worker_assigned[w] = bike_assigned[b] = True
result[w] = b
return result
Why This is Problematic: While this achieves O(nm) time complexity by avoiding comparison-based sorting, it:
- Requires knowledge of the distance constraint (not always given)
- Uses more memory for sparse buckets
- Is harder to understand and maintain
- The performance gain is minimal for typical input sizes
4. Forgetting Edge Cases
Not handling the case where n = m (equal workers and bikes):
Solution: The original approach handles this correctly since every worker will get exactly one bike when n ≤ m is guaranteed.
Best Practice Solution:
Stick with the simple sorting approach from the original solution. It's clear, correct, and efficient enough for the problem constraints. The code is maintainable and doesn't require special knowledge about distance bounds.
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
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
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
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!