1066. Campus Bikes II 🔒
Problem Description
You have a 2D grid representing a campus with n
workers and m
bikes positioned at various coordinates. The number of bikes is always greater than or equal to the number of workers (n ≤ m
).
Your task is to assign exactly one bike to each worker such that:
- Every worker gets exactly one unique bike
- No two workers share the same bike
- The total sum of Manhattan distances between all worker-bike pairs is minimized
The Manhattan distance between two points p1
and p2
is calculated as: |p1.x - p2.x| + |p1.y - p2.y|
For example, if a worker is at position (1, 2)
and a bike is at position (3, 5)
, the Manhattan distance would be |1-3| + |2-5| = 2 + 3 = 5
.
The solution uses dynamic programming with bitmask to track which bikes have been assigned. The state f[i][j]
represents the minimum total distance when assigning bikes to the first i
workers, where j
is a bitmask indicating which bikes have been used (if the k
-th bit of j
is 1, the k
-th bike has been assigned).
The algorithm iterates through each worker and considers all possible bike assignments, updating the minimum distance for each valid configuration. The final answer is the minimum value among all possible complete assignments for all n
workers.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: While we have workers and bikes on a 2D grid, this is not a graph traversal problem with nodes and edges. We're dealing with an assignment optimization problem.
Need to solve for kth smallest/largest?
- No: We're looking for the minimum total distance, not the kth smallest/largest element.
Involves Linked Lists?
- No: The problem involves 2D coordinates and assignments, not linked list operations.
Does the problem have small constraints?
- Yes: The constraint
n ≤ m
and the fact that we need to try different combinations of worker-bike assignments suggests relatively small constraints. Withn
workers andm
bikes, we need to explore different assignment combinations, which becomes computationally expensive for large values.
Brute force / Backtracking?
- Yes: We need to explore different ways to assign bikes to workers. Each worker can potentially be paired with any available bike, and we need to try different combinations to find the optimal assignment that minimizes the total Manhattan distance.
Conclusion: The flowchart suggests using a backtracking approach for this problem. We systematically try assigning each worker to different available bikes, keep track of used bikes, and backtrack when needed to explore other possibilities. The solution actually implements this using dynamic programming with bitmask (which is an optimized version of the backtracking idea), where the bitmask represents which bikes have been assigned, effectively encoding the backtracking state space.
Intuition
When we first look at this problem, the natural instinct is to try all possible ways to assign bikes to workers. With n
workers and m
bikes, we need to choose n
bikes from m
options and assign them to workers in a way that minimizes total distance.
The key insight is that this is fundamentally an assignment problem where:
- Each worker must get exactly one bike
- Each bike can only be assigned to one worker
- We want the optimal (minimum cost) assignment
If we think about solving this recursively, for each worker, we could try assigning them to any available bike, then solve the remaining subproblem. This naturally leads to a backtracking approach where we:
- Assign worker 0 to an available bike
- Recursively assign the remaining workers to remaining bikes
- Track the minimum total distance across all valid assignments
However, pure backtracking would involve a lot of repeated calculations. For instance, if workers 0 and 1 are assigned bikes 3 and 5, the optimal assignment for the remaining workers would be the same regardless of whether worker 0 got bike 3 or bike 5 (as long as the set of used bikes is the same).
This observation leads us to dynamic programming with states. The state we need to track is:
- Which worker we're currently assigning (worker index
i
) - Which bikes have already been used (a set of used bikes)
Since we need to track a subset of bikes, we can use a bitmask where each bit represents whether a bike is used (1) or available (0). For example, if bikes 0 and 2 are used, the bitmask would be 101
in binary or 5
in decimal.
The DP state f[i][j]
represents: "What's the minimum distance to assign bikes to the first i
workers, where j
(as a bitmask) indicates which bikes have been used?"
For each state, we try assigning the current worker to each available bike (where bit is 1 in the mask), calculate the distance, and add it to the optimal solution for the previous workers with that bike removed from the available set. This effectively explores all possible assignments while avoiding redundant calculations through memoization.
Learn more about Dynamic Programming, Backtracking and Bitmask patterns.
Solution Approach
The solution implements dynamic programming with bitmask to efficiently explore all possible worker-bike assignments.
Data Structure:
- A 2D DP table
f[i][j]
where:i
represents the number of workers we've assigned bikes to (from 0 ton
)j
is a bitmask representing which bikes have been usedf[i][j]
stores the minimum total Manhattan distance for this configuration
Initialization:
- Create a DP table with dimensions
(n+1) × (2^m)
initialized to infinity - Set
f[0][0] = 0
as the base case (0 workers assigned, no bikes used, distance is 0)
Main Algorithm:
- Iterate through each worker
i
from 1 ton
:- For each possible bike configuration
j
(from 0 to2^m - 1
):- Try assigning each bike
k
to the current worker - Check if bike
k
is available in the current mask:j >> k & 1
- This checks if the k-th bit is set to 1
- If bike
k
is available:- Calculate the Manhattan distance between worker
i-1
and bikek
- The previous state would be
f[i-1][j ^ (1 << k)]
j ^ (1 << k)
removes bikek
from the used set (flips the k-th bit)
- Update:
f[i][j] = min(f[i][j], f[i-1][previous_state] + distance)
- Calculate the Manhattan distance between worker
- Try assigning each bike
- For each possible bike configuration
Finding the Answer:
- After processing all workers, the answer is
min(f[n])
- This finds the minimum value among all possible ways to assign all
n
workers to bikes
Time Complexity: O(n × 2^m × m)
where we have n
workers, 2^m
possible bike configurations, and check m
bikes for each state.
Space Complexity: O(n × 2^m)
for the DP table.
The bitmask technique elegantly handles the constraint that each bike can only be used once, while the DP approach ensures we don't recalculate the same subproblems multiple times.
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), (2, 1), (2, 0)]
Step 1: Initialize DP Table
- Create
f[3][8]
table (3 rows for 0-2 workers, 8 columns for 2^3=8 bike configurations) - Initialize all values to infinity except
f[0][0] = 0
Step 2: Assign first worker (i=1)
For each possible mask where exactly 1 bike is used:
-
Mask
001
(bike 0 used):- Distance from worker 0
(0,0)
to bike 0(0,1)
= |0-0| + |0-1| = 1 f[1][001] = f[0][000] + 1 = 0 + 1 = 1
- Distance from worker 0
-
Mask
010
(bike 1 used):- Distance from worker 0
(0,0)
to bike 1(2,1)
= |0-2| + |0-1| = 3 f[1][010] = f[0][000] + 3 = 0 + 3 = 3
- Distance from worker 0
-
Mask
100
(bike 2 used):- Distance from worker 0
(0,0)
to bike 2(2,0)
= |0-2| + |0-0| = 2 f[1][100] = f[0][000] + 2 = 0 + 2 = 2
- Distance from worker 0
Step 3: Assign second worker (i=2)
For each mask where exactly 2 bikes are used:
-
Mask
011
(bikes 0,1 used):- Try bike 0 for worker 1: Previous state
f[1][010]
= 3- Distance: worker 1
(1,1)
to bike 0(0,1)
= 1 - Total: 3 + 1 = 4
- Distance: worker 1
- Try bike 1 for worker 1: Previous state
f[1][001]
= 1- Distance: worker 1
(1,1)
to bike 1(2,1)
= 1 - Total: 1 + 1 = 2
- Distance: worker 1
f[2][011] = min(4, 2) = 2
- Try bike 0 for worker 1: Previous state
-
Mask
101
(bikes 0,2 used):- Try bike 0 for worker 1: Previous state
f[1][100]
= 2- Distance: worker 1
(1,1)
to bike 0(0,1)
= 1 - Total: 2 + 1 = 3
- Distance: worker 1
- Try bike 2 for worker 1: Previous state
f[1][001]
= 1- Distance: worker 1
(1,1)
to bike 2(2,0)
= 2 - Total: 1 + 2 = 3
- Distance: worker 1
f[2][101] = min(3, 3) = 3
- Try bike 0 for worker 1: Previous state
-
Mask
110
(bikes 1,2 used):- Try bike 1 for worker 1: Previous state
f[1][100]
= 2- Distance: worker 1
(1,1)
to bike 1(2,1)
= 1 - Total: 2 + 1 = 3
- Distance: worker 1
- Try bike 2 for worker 1: Previous state
f[1][010]
= 3- Distance: worker 1
(1,1)
to bike 2(2,0)
= 2 - Total: 3 + 2 = 5
- Distance: worker 1
f[2][110] = min(3, 5) = 3
- Try bike 1 for worker 1: Previous state
Step 4: Find minimum
The answer is min(f[2])
= min(2, 3, 3, ...)
= 2
This corresponds to assigning worker 0 to bike 0 (distance 1) and worker 1 to bike 1 (distance 1), for a total minimum distance of 2.
Solution Implementation
1class Solution:
2 def assignBikes(self, workers: List[List[int]], bikes: List[List[int]]) -> int:
3 # Get the number of workers and bikes
4 num_workers = len(workers)
5 num_bikes = len(bikes)
6
7 # Initialize DP table
8 # dp[i][mask] = minimum distance to assign bikes to first i workers
9 # where mask represents which bikes have been used
10 dp = [[float('inf')] * (1 << num_bikes) for _ in range(num_workers + 1)]
11
12 # Base case: no workers assigned, no bikes used, distance is 0
13 dp[0][0] = 0
14
15 # Iterate through each worker (1-indexed for easier DP transition)
16 for worker_idx in range(1, num_workers + 1):
17 worker_x, worker_y = workers[worker_idx - 1]
18
19 # Try all possible bike assignment states
20 for bike_mask in range(1 << num_bikes):
21 # Try assigning each bike to current worker
22 for bike_idx in range(num_bikes):
23 # Check if this bike is already assigned in current mask
24 if (bike_mask >> bike_idx) & 1:
25 bike_x, bike_y = bikes[bike_idx]
26
27 # Calculate Manhattan distance between worker and bike
28 distance = abs(worker_x - bike_x) + abs(worker_y - bike_y)
29
30 # Previous state: one less worker, without current bike
31 prev_mask = bike_mask ^ (1 << bike_idx)
32
33 # Update minimum distance for current state
34 dp[worker_idx][bike_mask] = min(
35 dp[worker_idx][bike_mask],
36 dp[worker_idx - 1][prev_mask] + distance
37 )
38
39 # Return minimum distance among all valid assignments
40 # (all workers assigned, any combination of bikes)
41 return min(dp[num_workers])
42
1class Solution {
2 public int assignBikes(int[][] workers, int[][] bikes) {
3 int numWorkers = workers.length;
4 int numBikes = bikes.length;
5
6 // dp[i][mask] represents the minimum sum of distances when assigning
7 // the first i workers to bikes represented by the bitmask
8 int[][] dp = new int[numWorkers + 1][1 << numBikes];
9
10 // Initialize all states with a large value (infinity)
11 for (int[] row : dp) {
12 Arrays.fill(row, 1 << 30);
13 }
14
15 // Base case: 0 workers assigned to 0 bikes has distance 0
16 dp[0][0] = 0;
17
18 // Iterate through each worker
19 for (int workerIndex = 1; workerIndex <= numWorkers; workerIndex++) {
20 // Iterate through all possible bike assignment states
21 for (int bikeMask = 0; bikeMask < (1 << numBikes); bikeMask++) {
22 // Try assigning current worker to each available bike in the mask
23 for (int bikeIndex = 0; bikeIndex < numBikes; bikeIndex++) {
24 // Check if the bike at bikeIndex is available in current mask
25 if ((bikeMask >> bikeIndex & 1) == 1) {
26 // Calculate Manhattan distance between worker and bike
27 int distance = Math.abs(workers[workerIndex - 1][0] - bikes[bikeIndex][0])
28 + Math.abs(workers[workerIndex - 1][1] - bikes[bikeIndex][1]);
29
30 // Update dp: current state = previous state without this bike + distance
31 // bikeMask ^ (1 << bikeIndex) removes the current bike from mask
32 dp[workerIndex][bikeMask] = Math.min(
33 dp[workerIndex][bikeMask],
34 dp[workerIndex - 1][bikeMask ^ (1 << bikeIndex)] + distance
35 );
36 }
37 }
38 }
39 }
40
41 // Return the minimum distance among all valid assignments for all workers
42 return Arrays.stream(dp[numWorkers]).min().getAsInt();
43 }
44}
45
1class Solution {
2public:
3 int assignBikes(vector<vector<int>>& workers, vector<vector<int>>& bikes) {
4 int numWorkers = workers.size();
5 int numBikes = bikes.size();
6
7 // dp[i][mask] = minimum total distance to assign bikes to first i workers
8 // where mask represents which bikes have been used (1 = used, 0 = available)
9 int dp[numWorkers + 1][1 << numBikes];
10
11 // Initialize all values to a large number (infinity)
12 memset(dp, 0x3f, sizeof(dp));
13
14 // Base case: 0 workers assigned with no bikes used has distance 0
15 dp[0][0] = 0;
16
17 // Iterate through each worker
18 for (int workerIdx = 1; workerIdx <= numWorkers; ++workerIdx) {
19 // Try all possible bike assignment states
20 for (int bikeMask = 0; bikeMask < (1 << numBikes); ++bikeMask) {
21 // Try assigning each bike to the current worker
22 for (int bikeIdx = 0; bikeIdx < numBikes; ++bikeIdx) {
23 // Check if this bike is used in the current mask
24 if ((bikeMask >> bikeIdx) & 1) {
25 // Calculate Manhattan distance between worker and bike
26 int distance = abs(workers[workerIdx - 1][0] - bikes[bikeIdx][0]) +
27 abs(workers[workerIdx - 1][1] - bikes[bikeIdx][1]);
28
29 // Update minimum distance: current state comes from previous worker state
30 // without this bike (remove bike from mask using XOR)
31 dp[workerIdx][bikeMask] = min(dp[workerIdx][bikeMask],
32 dp[workerIdx - 1][bikeMask ^ (1 << bikeIdx)] + distance);
33 }
34 }
35 }
36 }
37
38 // Find minimum total distance among all valid bike assignments for all workers
39 // Valid assignments have exactly numWorkers bikes used
40 return *min_element(dp[numWorkers], dp[numWorkers] + (1 << numBikes));
41 }
42};
43
1/**
2 * Assigns bikes to workers minimizing the total Manhattan distance
3 * Uses dynamic programming with bitmask to track bike assignments
4 *
5 * @param workers - Array of worker positions [x, y]
6 * @param bikes - Array of bike positions [x, y]
7 * @returns Minimum sum of Manhattan distances for optimal assignment
8 */
9function assignBikes(workers: number[][], bikes: number[][]): number {
10 const workerCount: number = workers.length;
11 const bikeCount: number = bikes.length;
12 const INFINITY: number = 1 << 30; // Large value representing infinity
13
14 // DP table: dp[i][mask] represents minimum distance to assign first i workers
15 // where mask represents which bikes have been used (bit representation)
16 const dp: number[][] = new Array(workerCount + 1)
17 .fill(0)
18 .map(() => new Array(1 << bikeCount).fill(INFINITY));
19
20 // Base case: no workers assigned, no bikes used, distance is 0
21 dp[0][0] = 0;
22
23 // Iterate through each worker
24 for (let workerIndex = 1; workerIndex <= workerCount; ++workerIndex) {
25 // Iterate through all possible bike assignment states
26 for (let bikeMask = 0; bikeMask < (1 << bikeCount); ++bikeMask) {
27 // Try assigning each bike to current worker
28 for (let bikeIndex = 0; bikeIndex < bikeCount; ++bikeIndex) {
29 // Check if bike at bikeIndex is used in current mask
30 if (((bikeMask >> bikeIndex) & 1) === 1) {
31 // Calculate Manhattan distance between worker and bike
32 const manhattanDistance: number =
33 Math.abs(workers[workerIndex - 1][0] - bikes[bikeIndex][0]) +
34 Math.abs(workers[workerIndex - 1][1] - bikes[bikeIndex][1]);
35
36 // Previous state: one less worker, without current bike
37 const previousMask: number = bikeMask ^ (1 << bikeIndex);
38
39 // Update minimum distance for current state
40 dp[workerIndex][bikeMask] = Math.min(
41 dp[workerIndex][bikeMask],
42 dp[workerIndex - 1][previousMask] + manhattanDistance
43 );
44 }
45 }
46 }
47 }
48
49 // Return minimum distance among all valid bike assignments for all workers
50 return Math.min(...dp[workerCount]);
51}
52
Time and Space Complexity
Time Complexity: O(n * 2^m * m)
The algorithm uses dynamic programming with bitmask to track which bikes have been assigned. There are three nested loops:
- The outer loop iterates through
n
workers:O(n)
- The middle loop iterates through all possible bike assignment states (bitmasks):
O(2^m)
- The inner loop iterates through
m
bikes to check each possible assignment:O(m)
For each combination, we perform constant time operations (checking if a bike is used via bit manipulation, calculating Manhattan distance, and updating the minimum). Therefore, the overall time complexity is O(n * 2^m * m)
.
Space Complexity: O(n * 2^m)
The algorithm uses a 2D DP table f
where:
- The first dimension represents workers (from 0 to n):
n + 1
rows - The second dimension represents all possible bike assignment states using bitmask:
2^m
columns
Each cell stores a single value (the minimum distance), so the total space required is O((n + 1) * 2^m)
, which simplifies to O(n * 2^m)
.
Additional space usage includes the input lists and a few variables for indices and distances, but these don't affect the asymptotic space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Bit Manipulation Logic
A frequent mistake is confusing when a bit represents "used" vs "available". In the provided solution, when (bike_mask >> bike_idx) & 1
returns 1, it means the bike IS used in the current state. However, developers often mistakenly think this means the bike is available to assign.
Wrong interpretation:
# Incorrect: thinking bit=1 means available if (bike_mask >> bike_idx) & 1: # This actually means bike is USED # Try to assign this bike (WRONG!)
Correct approach:
# Check if bike is used in current mask if (bike_mask >> bike_idx) & 1: # Bike IS used in current state # Calculate previous state where this bike wasn't used prev_mask = bike_mask ^ (1 << bike_idx) # Remove this bike
2. Inefficient State Enumeration
Another pitfall is iterating through all possible masks (0 to 2^m - 1) even when many states are invalid. Since we're assigning exactly i
bikes to i
workers, we should only consider masks with exactly i
bits set.
Inefficient version:
for bike_mask in range(1 << num_bikes): # Checks ALL possible masks
# Many of these masks have wrong number of bikes selected
Optimized solution:
# Only iterate through masks with exactly i bikes selected
for bike_mask in range(1 << num_bikes):
if bin(bike_mask).count('1') == worker_idx: # Only valid states
for bike_idx in range(num_bikes):
if (bike_mask >> bike_idx) & 1:
# Process valid assignment
3. Memory Overflow with Large Input
When the number of bikes is large (e.g., m = 20), the DP table size becomes 21 × 2^20, which can cause memory issues. Consider using rolling array optimization or memoization with dictionary.
Memory-efficient alternative using dictionary:
from functools import lru_cache
@lru_cache(maxsize=None)
def dp(worker_idx, bike_mask):
if worker_idx == num_workers:
return 0
min_dist = float('inf')
for bike_idx in range(num_bikes):
if not (bike_mask >> bike_idx) & 1: # Bike not used
dist = manhattan_distance(workers[worker_idx], bikes[bike_idx])
min_dist = min(min_dist, dist + dp(worker_idx + 1, bike_mask | (1 << bike_idx)))
return min_dist
4. Index Confusion Between Workers and DP Table
The DP table uses 1-based indexing for workers (dp[1] for first worker) while the workers array uses 0-based indexing. This often leads to off-by-one errors.
Common mistake:
# Wrong: forgetting to subtract 1 worker_x, worker_y = workers[worker_idx] # Should be worker_idx - 1
Correct indexing:
# Correct: properly adjust index worker_x, worker_y = workers[worker_idx - 1] # DP uses 1-based, array uses 0-based
What's the output of running the following function using the following tree as input?
1def serialize(root):
2 res = []
3 def dfs(root):
4 if not root:
5 res.append('x')
6 return
7 res.append(root.val)
8 dfs(root.left)
9 dfs(root.right)
10 dfs(root)
11 return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4 StringJoiner res = new StringJoiner(" ");
5 serializeDFS(root, res);
6 return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10 if (root == null) {
11 result.add("x");
12 return;
13 }
14 result.add(Integer.toString(root.val));
15 serializeDFS(root.left, result);
16 serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2 let res = [];
3 serialize_dfs(root, res);
4 return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8 if (!root) {
9 res.push("x");
10 return;
11 }
12 res.push(root.val);
13 serialize_dfs(root.left, res);
14 serialize_dfs(root.right, res);
15}
16
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
Want a Structured Path to Master System Design Too? Don’t Miss This!