1029. Two City Scheduling - Explanation

Problem Link

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: 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: 1859

Example 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.length
  • 2 <= costs.length <= 100
  • costs.length is even.
  • 1 <= aCost[i], bCost[i] <= 1000


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



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

  1. For each person at index i, try sending them to city A (if slots remain) and to city B (if slots remain).
  2. Recursively compute the minimum cost for the remaining people.
  3. Return the minimum of both choices.
  4. Base case: when all people are assigned (i == len(costs)), return 0.
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: O(2N)O(2 ^ N)
  • Space complexity: O(N)O(N) for recursion stack.

Where NN is the size of the array costscosts.


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

  1. Create a 2D memoization table dp[aCount][bCount] to store the minimum cost when aCount slots remain for city A and bCount slots remain for city B.
  2. Use the same recursive logic as before, but check the cache before computing.
  3. Store results in the cache before returning.
  4. The person index i can be derived from aCount + bCount since the total remaining equals 2n - 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: O(n2)O(n ^ 2)
  • Space complexity: O(n2)O(n ^ 2)

Where nn is the half of the size of the array costscosts.


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

  1. Initialize dp[0][0] = 0 (no people assigned, zero cost).
  2. For each state (aCount, bCount):
    • Compute the person index as i = aCount + bCount.
    • If aCount > 0, consider the option of sending person i-1 to city A.
    • If bCount > 0, consider the option of sending person i-1 to city B.
    • Take the minimum of valid options.
  3. 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: O(n2)O(n ^ 2)
  • Space complexity: O(n2)O(n ^ 2)

Where nn is the half of the size of the array costscosts.


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

  1. Use a 1D array dp of size n+1.
  2. Iterate through all (aCount, bCount) pairs in the same order as before.
  3. Use a temporary variable to store the previous value of dp[bCount] before overwriting.
  4. Update dp[bCount] using the temporary variable (for the city A option) and dp[bCount-1] (for the city B option).
  5. 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: O(n2)O(n ^ 2)
  • Space complexity: O(n)O(n)

Where nn is the half of the size of the array costscosts.


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

  1. For each person, compute the tuple (cost[B] - cost[A], cost[A], cost[B]).
  2. Sort all tuples by the difference (ascending).
  3. Send the first n people (smallest differences) to city B.
  4. Send the remaining n people to city A.
  5. 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 res

Time & Space Complexity

  • Time complexity: O(nlogn)O(n \log n)
  • Space complexity: O(n)O(n)

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

  1. Sort the costs array by cost[1] - cost[0] (ascending).
  2. For the first n people, add their city B cost.
  3. For the last n people, add their city A cost.
  4. Return the total.
class Solution:
    def twoCitySchedCost(self, costs: List[List[int]]) -> int:
        costs.sort(key=lambda x: x[1] - x[0])
        n, res = len(costs) // 2, 0

        for i in range(n):
            res += costs[i][1] + costs[i + n][0]
        return res

Time & Space Complexity

  • Time complexity: O(nlogn)O(n \log n)
  • Space complexity: O(1)O(1) or O(n)O(n) 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.