1029. Two City Scheduling - Explanation
Description
A company is planning to interview 2n people. Given the array costs where costs[i] = [aCost[i], bCost[i]], the cost of flying the i-th person to city a is aCost[i], and the cost of flying the i-th person to city b is bCost[i].
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: 110Explanation:
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: 3086Constraints:
2 * n == costs.length2 <= costs.length <= 100costs.lengthis even.1 <= aCost[i], bCost[i] <= 1000
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Recursion - Exploring all possible assignments by making choices at each step and backtracking
- Dynamic Programming - Using memoization to cache overlapping subproblems and avoid redundant computation
- Greedy algorithms - Making locally optimal choices (sorting by cost difference) to achieve a global optimum
- Sorting - Understanding custom comparators and how sorting by a derived value can enable optimal selection
1. Recursion
Intuition
We need to send exactly n people to city A and n people to city B, minimizing total cost. For each person, we have two choices: send them to A or B. We can explore all possible assignments using recursion, tracking how many slots remain for each city.
Algorithm
- For each person at index
i, try sending them to city A (if slots remain) and to city B (if slots remain). - Recursively compute the minimum cost for the remaining people.
- Return the minimum of both choices.
- Base case: when all people are assigned (
i == len(costs)), return0.
class Solution:
def twoCitySchedCost(self, costs: List[List[int]]) -> int:
n = len(costs) // 2
def dfs(i, aCount, bCount):
if i == len(costs):
return 0
res = float("inf")
if aCount > 0:
res = costs[i][0] + dfs(i + 1, aCount - 1, bCount)
if bCount > 0:
res = min(res, costs[i][1] + dfs(i + 1, aCount, bCount - 1))
return res
return dfs(0, n, n)Time & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
Where is the size of the array .
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution has overlapping subproblems because the same state (remaining slots for A and B) can be reached through different paths. We can use memoization to cache results and avoid redundant computation.
Algorithm
- Create a 2D memoization table
dp[aCount][bCount]to store the minimum cost whenaCountslots remain for city A andbCountslots remain for city B. - Use the same recursive logic as before, but check the cache before computing.
- Store results in the cache before returning.
- The person index
ican be derived fromaCount + bCountsince the total remaining equals2n - i.
class Solution:
def twoCitySchedCost(self, costs: List[List[int]]) -> int:
n = len(costs) // 2
dp = [[-1] * (n + 1) for _ in range(n + 1)]
def dfs(i, aCount, bCount):
if i == len(costs):
return 0
if dp[aCount][bCount] != -1:
return dp[aCount][bCount]
res = float("inf")
if aCount > 0:
res = costs[i][0] + dfs(i + 1, aCount - 1, bCount)
if bCount > 0:
res = min(res, costs[i][1] + dfs(i + 1, aCount, bCount - 1))
dp[aCount][bCount] = res
return res
return dfs(0, n, n)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the half of the size of the array .
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of recursion with memoization, we can build the solution iteratively. We fill a table where dp[aCount][bCount] represents the minimum cost to assign the first aCount + bCount people such that aCount go to city A and bCount go to city B.
Algorithm
- Initialize
dp[0][0] = 0(no people assigned, zero cost). - For each state
(aCount, bCount):- Compute the person index as
i = aCount + bCount. - If
aCount > 0, consider the option of sending personi-1to city A. - If
bCount > 0, consider the option of sending personi-1to city B. - Take the minimum of valid options.
- Compute the person index as
- Return
dp[n][n].
class Solution:
def twoCitySchedCost(self, costs: List[List[int]]) -> int:
n = len(costs) // 2
dp = [[0] * (n + 1) for _ in range(n + 1)]
for aCount in range(n + 1):
for bCount in range(n + 1):
i = aCount + bCount
if i == 0:
continue
dp[aCount][bCount] = float("inf")
if aCount > 0:
dp[aCount][bCount] = min(dp[aCount][bCount], dp[aCount - 1][bCount] + costs[i - 1][0])
if bCount > 0:
dp[aCount][bCount] = min(dp[aCount][bCount], dp[aCount][bCount - 1] + costs[i - 1][1])
return dp[n][n]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the half of the size of the array .
4. Dynamic Programming (Space Optimized)
Intuition
Looking at the bottom-up recurrence, each cell dp[aCount][bCount] only depends on dp[aCount-1][bCount] and dp[aCount][bCount-1]. We can reduce space by using a 1D array and carefully updating it in the right order.
Algorithm
- Use a 1D array
dpof sizen+1. - Iterate through all
(aCount, bCount)pairs in the same order as before. - Use a temporary variable to store the previous value of
dp[bCount]before overwriting. - Update
dp[bCount]using the temporary variable (for the city A option) anddp[bCount-1](for the city B option). - Return
dp[n].
class Solution:
def twoCitySchedCost(self, costs: List[List[int]]) -> int:
n = len(costs) // 2
dp = [0] * (n + 1)
for aCount in range(n + 1):
for bCount in range(n + 1):
i = aCount + bCount
if i == 0:
continue
tmp = dp[bCount]
dp[bCount] = float("inf")
if aCount > 0:
dp[bCount] = min(dp[bCount], tmp + costs[i - 1][0])
if bCount > 0:
dp[bCount] = min(dp[bCount], dp[bCount - 1] + costs[i - 1][1])
return dp[n]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the half of the size of the array .
5. Greedy
Intuition
For each person, the "cost difference" cost[B] - cost[A] tells us how much extra we pay to send them to city B instead of A. A negative difference means B is cheaper. If we sort by this difference, the first half of people have the smallest (most negative) differences, meaning they benefit most from going to city B.
Algorithm
- For each person, compute the tuple
(cost[B] - cost[A], cost[A], cost[B]). - Sort all tuples by the difference (ascending).
- Send the first
npeople (smallest differences) to city B. - Send the remaining
npeople to city A. - Sum up all the costs.
class Solution:
def twoCitySchedCost(self, costs: List[List[int]]) -> int:
diffs = []
for c1, c2 in costs:
diffs.append([c2 - c1, c1, c2])
diffs.sort()
res = 0
for i in range(len(diffs)):
if i < len(diffs) // 2:
res += diffs[i][2]
else:
res += diffs[i][1]
return resTime & Space Complexity
- Time complexity:
- Space complexity:
6. Greedy (Optimal)
Intuition
We can simplify the greedy approach by sorting the original array directly by cost[B] - cost[A] without creating extra tuples. After sorting, the first n entries favor city B, and the last n favor city A.
Algorithm
- Sort the
costsarray bycost[1] - cost[0](ascending). - For the first
npeople, add their city B cost. - For the last
npeople, add their city A cost. - Return the total.
Time & Space Complexity
- Time complexity:
- Space complexity: or depending on the sorting algorithm.
Common Pitfalls
Sorting by the Wrong Cost Difference
The greedy approach requires sorting by cost[B] - cost[A] to determine which people benefit most from going to city B. A common mistake is sorting by cost[A] - cost[B] or by individual costs rather than the difference. This leads to suboptimal assignments because you are not correctly identifying the relative advantage of each city for each person.
Not Enforcing Equal Distribution to Both Cities
The problem requires exactly n people to go to city A and n people to go to city B. A greedy approach that simply sends each person to their cheaper city without tracking counts will likely result in an unbalanced distribution. Always ensure your solution enforces the constraint that exactly half go to each city.
Off-by-One Errors in Index Calculation for DP
In the bottom-up DP approach, the person index i is derived from aCount + bCount. A common error is using i instead of i - 1 when accessing the costs array, or incorrectly computing which person corresponds to a given state. This leads to accessing wrong cost values or array index out of bounds errors.
Sign in to join the discussion