Two City Scheduling
MedPrompt
A company is planning to interview 2n people. Given the array costs where costs[i] = [aCosti, bCosti], the cost of flying the ith person to city a is aCosti, and the cost of flying the ith person to city b is bCosti.
Return the minimum cost to fly every person to a city such that exactly n people arrive in each city.
Example 1:
Input: costs = [[10,20],[30,200],[400,50],[30,20]]
Output: 110
Explanation:
The first person goes to city A for a cost of 10.
The second person goes to city A for a cost of 30.
The third person goes to city B for a cost of 50.
The fourth person goes to city B for a cost of 20.
The total minimum cost is 10 + 30 + 50 + 20 = 110 to have half the people interviewing in each city.Example 2:
Input: costs = [[259,770],[448,54],[926,667],[184,139],[840,118],[577,469]]
Output: 1859Example 3:
Input: costs = [[515,563],[451,713],[537,709],[343,819],[855,779],[457,60],[650,359],[631,42]]
Output: 3086
Constraints:
2 * n == costs.length2 <= costs.length <= 100costs.lengthis even.1 <= aCosti, bCosti <= 1000
Approaches
3 approaches with complexity analysis and trade-offs.
This approach explores every possible valid assignment of people to cities. We can define a recursive function that tries to assign each person to either city A or city B, while keeping track of the number of people assigned to each city. We backtrack and explore all combinations that result in exactly n people in each city and find the one with the minimum total cost.
Algorithm
- Define a recursive function
calculate(index, countA, countB)that computes the minimum cost for people fromindexto2n-1. - The parameters
countAandcountBtrack the number of people already assigned to city A and city B, respectively. - Base Case: If
indexreaches2n(all people assigned), check ifcountA == nandcountB == n. If so, this is a valid assignment, and the cost from this point is 0. Otherwise, it's an invalid assignment, so return a very large value. - Recursive Step: For the person at
index, explore two possibilities:- Send to City A: If
countA < n, recursively callcalculate(index + 1, countA + 1, countB)and addcosts[index][0]. - Send to City B: If
countB < n, recursively callcalculate(index + 1, countA, countB + 1)and addcosts[index][1].
- Send to City A: If
- Return the minimum cost from the valid choices.
- The initial call is
calculate(0, 0, 0).
Walkthrough
We can model this problem as finding the minimum cost path in a decision tree. We define a recursive function, say calculate(index, countA, countB), which calculates the minimum cost for people from index to 2n-1, given that countA people have already been assigned to city A and countB to city B.
The state of our recursion is (index, countA, countB).
- Base Case: If
indexreaches2n, all people have been assigned. IfcountA == nandcountB == n, we have a valid assignment, and the cost from this point is 0. Otherwise, it's an invalid assignment, so we return a very large value to signify this path should not be chosen. - Recursive Step: For the person at
index, we have two choices:- Send to City A: If
countA < n, we can send this person to city A. The cost would becosts[index][0] + calculate(index + 1, countA + 1, countB). - Send to City B: If
countB < n, we can send this person to city B. The cost would becosts[index][1] + calculate(index + 1, countA, countB + 1).
- Send to City A: If
We take the minimum of the costs of the available choices. The initial call to the function would be calculate(0, 0, 0). To avoid recomputing, memoization can be used, which transitions this solution to the dynamic programming approach.
class Solution { public int twoCitySchedCost(int[][] costs) { // Using a memoization table would optimize this to a DP solution. // int[][][] memo = new int[costs.length][costs.length/2 + 1][costs.length/2 + 1]; return calculate(costs, 0, 0, 0); } private int calculate(int[][] costs, int index, int countA, int countB) { int n = costs.length / 2; if (index == costs.length) { // If we have exactly n people in each city, we found a valid solution. return 0; } int costToA = Integer.MAX_VALUE; if (countA < n) { costToA = costs[index][0] + calculate(costs, index + 1, countA + 1, countB); } int costToB = Integer.MAX_VALUE; if (countB < n) { costToB = costs[index][1] + calculate(costs, index + 1, countA, countB + 1); } return Math.min(costToA, costToB); }}Complexity
Time
O(2^(2n)). While pruning helps, the number of valid combinations is C(2n, n), which is exponential. For n=50, this is prohibitively large.
Space
O(n), for the recursion stack depth, which can go up to 2n.
Trade-offs
Pros
Simple to conceptualize and follows the problem statement directly.
Cons
Extremely inefficient due to its exponential time complexity.
Will result in a 'Time Limit Exceeded' error for the given constraints.
A more optimized approach is to use dynamic programming to avoid re-computing results for the same subproblems. We can define a DP state dp[i][j] as the minimum cost to assign the first i people such that j of them are sent to city A. The final answer would be the state where all 2n people are assigned, with n in city A.
Algorithm
- Create a 2D DP table,
dp[i][j], to store the minimum cost for assigning the firstipeople withjof them going to city A. - The table size will be
(2n + 1) x (n + 1). - Initialize the table with a large value and set the base case
dp[0][0] = 0. - Iterate
ifrom 1 to2n(representing the people). - Iterate
jfrom 0 ton(representing the count of people in city A). - For each
dp[i][j], calculate the cost based on two possibilities for thei-th person:- Goes to City A:
dp[i-1][j-1] + costs[i-1][0]. This is valid ifj > 0. - Goes to City B:
dp[i-1][j] + costs[i-1][1]. This is valid if the number of people in city B (i-j) does not exceedn.
- Goes to City A:
- Set
dp[i][j]to the minimum of the valid possibilities. - The final answer is
dp[2n][n].
Walkthrough
This approach uses dynamic programming to solve the problem by breaking it down into smaller, overlapping subproblems. We define dp[i][j] as the minimum cost to schedule the first i people, with exactly j of them assigned to city A. Consequently, i-j people are assigned to city B.
The state transition is as follows: To compute dp[i][j], we consider the i-th person (at index i-1 in the costs array). This person can be assigned to either city A or city B.
- Assign person
i-1to City A: This is possible if we havej-1people assigned to city A among the firsti-1people. The cost would bedp[i-1][j-1] + costs[i-1][0]. This choice is valid only ifj > 0. - Assign person
i-1to City B: This is possible if we havejpeople assigned to city A among the firsti-1people. The cost would bedp[i-1][j] + costs[i-1][1]. This choice is valid only if the number of people in city B,i-j, does not exceedn.
The value of dp[i][j] is the minimum of these two options. The DP table is of size (2n+1) x (n+1). The base case is dp[0][0] = 0. The final answer is dp[2n][n].
class Solution { public int twoCitySchedCost(int[][] costs) { int n = costs.length / 2; int[][] dp = new int[2 * n + 1][n + 1]; for (int i = 0; i <= 2 * n; i++) { for (int j = 0; j <= n; j++) { dp[i][j] = Integer.MAX_VALUE / 2; } } dp[0][0] = 0; for (int i = 1; i <= 2 * n; i++) { for (int j = 0; j <= n; j++) { // Option 1: Person i-1 goes to city A int costA = Integer.MAX_VALUE / 2; if (j > 0) { costA = dp[i - 1][j - 1] + costs[i - 1][0]; } // Option 2: Person i-1 goes to city B int costB = Integer.MAX_VALUE / 2; if (i - j <= n) { // Number of people in city B must not exceed n costB = dp[i - 1][j] + costs[i - 1][1]; } dp[i][j] = Math.min(costA, costB); } } return dp[2 * n][n]; }}Complexity
Time
O(n^2), as we iterate through a DP table of size approximately 2n * n.
Space
O(n^2) for the DP table. This can be optimized to O(n) since each state `dp[i]` only depends on the previous state `dp[i-1]`.
Trade-offs
Pros
Guarantees an optimal solution.
Much more efficient than brute force and passes within time limits.
Cons
Slightly more complex to implement than the greedy approach.
Not as efficient as the greedy solution in terms of time complexity.
The most efficient approach is a greedy one. The core idea is to determine for each person the 'benefit' of sending them to city B instead of city A. This benefit can be calculated as costB - costA. To minimize the total cost, we should send the n people with the lowest costB - costA difference to city B, and the rest to city A.
Algorithm
- The key insight is to quantify the cost savings of sending a person to city B over city A. This is
costs[i][1] - costs[i][0]. - Sort the
costsarray in ascending order based on this difference. - The first
npeople in the sorted list are those for whom sending to city B is most advantageous (or least disadvantageous). These people should be sent to city B. - The remaining
npeople are sent to city A. - Initialize a
totalCostvariable to 0. - Iterate through the sorted
costsarray:- For the first
npeople (indices 0 ton-1), add their city B cost (costs[i][1]) tototalCost. - For the next
npeople (indicesnto2n-1), add their city A cost (costs[i][0]) tototalCost.
- For the first
- Return
totalCost.
Walkthrough
The problem can be reframed to make a greedy choice apparent. Suppose we tentatively send everyone to city A. The total cost would be the sum of all aCost_i. However, we must send n people to city B. When we change our decision for a person i from city A to city B, the total cost changes by bCost_i - aCost_i. Let's call this value the 'refund' for person i.
To minimize the final total cost, we should make this change for the n people who give us the best 'refund', i.e., those with the smallest bCost_i - aCost_i value. A negative refund is actually a saving, which is even better.
So, the strategy is to calculate this difference for every person, sort the people based on this difference, and send the n people with the smallest differences to city B. The other n people (with the largest differences) are sent to city A.
The total cost is then the sum of bCost for the first n people in the sorted list and aCost for the remaining n people.
import java.util.Arrays;import java.util.Comparator; class Solution { public int twoCitySchedCost(int[][] costs) { // Sort by the difference in costs, which represents the 'refund' // for sending a person to city B instead of city A. Arrays.sort(costs, new Comparator<int[]>() { @Override public int compare(int[] a, int[] b) { return (a[1] - a[0]) - (b[1] - b[0]); } }); int minCost = 0; int n = costs.length / 2; // The first n people have the smallest bCost - aCost, // so they should go to city B to maximize the 'refund'. for (int i = 0; i < n; i++) { minCost += costs[i][1]; } // The remaining n people have a larger bCost - aCost, // so it's cheaper to send them to city A. for (int i = n; i < 2 * n; i++) { minCost += costs[i][0]; } return minCost; }}Complexity
Time
O(n log n), dominated by the sorting step of 2n elements.
Space
O(log n) or O(n), depending on the sorting algorithm's implementation details (e.g., space for recursion stack or auxiliary arrays). In Java, `Arrays.sort` for objects uses Timsort which can take O(n) space.
Trade-offs
Pros
Most efficient solution with O(n log n) time complexity.
Relatively easy to understand and implement once the greedy insight is clear.
Cons
The greedy choice's correctness is not immediately obvious without a proof.
Solutions
Solution
class Solution { public int twoCitySchedCost ( int [][] costs ) { Arrays . sort ( costs , ( a , b ) -> { return a [ 0 ] - a [ 1 ] - ( b [ 0 ] - b [ 1 ]); }); int ans = 0 ; int n = costs . length >> 1 ; for ( int i = 0 ; i < n ; ++ i ) { ans += costs [ i ][ 0 ] + costs [ i + n ][ 1 ]; } return ans ; } }class Solution { public: int twoCitySchedCost ( vector < vector < int >>& costs ) { sort ( costs . begin (), costs . end (), []( const vector < int >& a , const vector < int >& b ) { return a [ 0 ] - a [ 1 ] < b [ 0 ] - b [ 1 ]; }); int n = costs . size () / 2 ; int ans = 0 ; for ( int i = 0 ; i < n ; ++ i ) { ans += costs [ i ][ 0 ] + costs [ i + n ][ 1 ]; } return ans ; } };class Solution : def twoCitySchedCost ( self , costs : List [ List [ int ]]) -> int : costs . sort ( key = lambda x : x [ 0 ] - x [ 1 ]) n = len ( costs ) >> 1 return sum ( costs [ i ][ 0 ] + costs [ i + n ][ 1 ] for i in range ( n ))Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.