Two City Scheduling
GREEDY · MEDIUM
The problem
Section titled “The problem”A company is interviewing 2n people. Person i costs costs[i][0] dollars to fly to city A, or costs[i][1] dollars to fly to city B. Exactly n people must end up in each city. Return the minimum total cost of flying everyone so that split is respected.
Every assignment is a choice of which n of the 2n people fly to city A — everyone else flies to city B. The question is which n to pick.
Solution 1: Brute force — try every possible split
Section titled “Solution 1: Brute force — try every possible split”The most literal reading of the problem: enumerate every possible group of n people to send to city A (the rest automatically go to city B), total the cost for each grouping, and keep the minimum. This is correct by definition — it is exactly what “minimum cost over all valid assignments” means — but the number of groupings is C(2n, n), which grows combinatorially. It’s only implemented and animated here for a tiny 4-person example; it would never scale to real input sizes.
costs = [[10, 20], [30, 200], [400, 50], [30, 20]] — each bar is one person's cost under the current A/B assignment
Press Play or Step to begin.
Combination being tried · best cost so far
Step 0 / 8
The code
Section titled “The code”function combinations(arr, k) { const results = []; function backtrack(start, chosen) { if (chosen.length === k) { results.push(chosen.slice()); return; } for (let i = start; i < arr.length; i++) { chosen.push(arr[i]); backtrack(i + 1, chosen); chosen.pop(); } } backtrack(0, []); return results;}
function twoCityBruteForce(costs) { const total = costs.length; const n = total / 2; const indices = costs.map((_, i) => i); const groupsA = combinations(indices, n);
let best = Infinity; for (const groupA of groupsA) { const setA = new Set(groupA); let cost = 0; for (let i = 0; i < total; i++) { cost += setA.has(i) ? costs[i][0] : costs[i][1]; } best = Math.min(best, cost); } return best;}Line by line:
combinations(indices, n)generates every way to choosenindices out of2nto represent “these people fly to city A” — there areC(2n, n)of them.- For each candidate group,
setA.has(i)decides per person whether to chargecosts[i][0](city A) orcosts[i][1](city B) — everyone not chosen for A automatically goes to B, so the split is always exactlynandn. best = Math.min(best, cost)just keeps the cheapest total seen across every group — no shortcuts, no pruning.
Complexity
Section titled “Complexity”| Time | Space | Approach |
|---|---|---|
| O(C(2n, n) · n) — combinatorial | O(n) | Enumerate every valid A/B split, sum, keep the min |
Solution 2: Optimal — greedy sort by savings
Section titled “Solution 2: Optimal — greedy sort by savings”Reframe the decision: sending person i to city A instead of city B changes the cost by aCost − bCost. If that number is very negative, A is a big bargain for that person; if it’s very positive, B is the bargain. To minimize the total, greedily send the n people with the smallest aCost − bCost (the biggest savings from choosing A) to city A, and send the rest to city B. Sort once by that difference, ascending, and split the sorted array down the middle — no combinatorial search needed.
costs = [[10, 20], [30, 200], [400, 50], [30, 20]] · Bars show aCost − bCost for each person in sorted order
Press Play or Step to begin.
Current person · diff value · assignment · running total
Step 0 / 7
The code
Section titled “The code”function twoCityOptimal(costs) { const n = costs.length / 2; const sorted = costs.slice().sort((a, b) => (a[0] - a[1]) - (b[0] - b[1]));
let total = 0; for (let i = 0; i < sorted.length; i++) { total += i < n ? sorted[i][0] : sorted[i][1]; } return total;}Line by line:
costs.slice()copies the array first — sorting in place would silently reorder the caller’s originalcosts, which is rarely what’s wanted..sort((a, b) => (a[0] - a[1]) - (b[0] - b[1]))orders people ascending byaCost − bCost: the biggest A-savings first, the biggest B-savings last.i < nis the entire “decision” step — the firstnpeople in sorted order fly to A, everyone after flies to B. Because the array is sorted by exactly the quantity that matters, this split is provably optimal — swapping any A/B pair across the boundary can only make the total equal or worse, never better.
Complexity
Section titled “Complexity”| Time | Space | Approach |
|---|---|---|
| O(n log n) | O(n) | Sort by aCost − bCost, split the sorted array in half |
Complexity comparison
Section titled “Complexity comparison”| Solution | Time | Space |
|---|---|---|
| Brute force (enumerate every split) | O(C(2n, n) · n) — combinatorial | O(n) |
| Optimal (greedy sort by savings) | O(n log n) | O(n) |
Honest caveat: the brute-force code above is only ever run against the small 4-person example on this page. C(2n, n) already reaches 184,756 at 2n=20 and billions well before 2n=60 — it is not a “slow but usable” fallback, it is asymptotically infeasible for any realistic input size.
Interview corner
Section titled “Interview corner”Why does sorting by aCost − bCost guarantee the optimal split, instead of just being a reasonable heuristic?
- Every valid assignment sends exactly
npeople to A andnto B — the total cost is (sum of everyone’s bCost) + (sum of aCost − bCost for whoever was sent to A). - The first term is fixed no matter which
npeople are chosen, so minimizing the total means minimizing the sum ofaCost − bCostover the chosen A-group. - To minimize a sum of
nvalues picked out of2n, pick thensmallest — which is exactly “sort ascending by diff, take the first n”. That’s a proof, not a guess.
What’s an exchange-argument way to convince an interviewer this greedy choice can’t be beaten?
- Suppose an optimal solution sends person X to B and person Y to A, but X’s diff (
aCost − bCost) is smaller than Y’s. - Swap them: send X to A and Y to B instead. The total cost changes by
(X's diff) − (Y's diff), which is negative or zero since X’s diff is smaller — the swap never makes things worse, and strictly improves them if the diffs differ. - So any solution that isn’t sorted-by-diff can be transformed into one that is, without increasing cost — meaning the sorted split is always at least as good as any alternative.
How would you extend this if the two cities didn’t need exactly equal n/n splits — say, city A needs exactly k people for some given k?
- The same proof still holds — it never depended on
kbeing exactly half. - Sort by
aCost − bCostascending, send the firstkto A, the remaining2n − kto B. - The only real requirement is that
kpeople go to A and the rest go to B — the greedy argument (swap any A/B pair sorted “wrong” without raising the total) applies unchanged. - ↳ Common follow-up: “What if a person can only be sent to one specific city (a hard constraint)?” That breaks the simple sort — it becomes closer to an assignment/flow problem, since some people are no longer free to be greedily reassigned.
Trick question — what does this log?
const costs = [[10, 20], [30, 200], [400, 50], [30, 20]];const sorted = costs.slice().sort();console.log(sorted[0]);- Answer:
[10, 20]— which happens to look plausible, but the whole array is now wrong:sortedis[[10,20],[30,20],[30,200],[400,50]], not sorted by diff at all. .sort()with no comparator converts every element to a string and compares them lexicographically.[400, 50]becomes the string"400,50", which sorts before"30,20"— treating 4 as “less than” 3 is nonsense for numbers, but that’s how string comparison works.- It happens to put the actual answer first here purely by coincidence of these specific numbers — it is not doing anything close to sorting by
aCost − bCost. - ⚠ Trap: this is exactly why the real solution always passes an explicit comparator,
(a, b) => (a[0] - a[1]) - (b[0] - b[1])— leaving it off doesn’t throw an error, it silently produces a different (and here, wrong) order.
1. Why does minimizing total cost reduce to minimizing the sum of (aCost − bCost) over just the people sent to city A?
- Because the sum of everyone’s bCost is the same for every valid split, so only the A-group’s diff sum varies
- Because aCost always equals bCost for half the people
- It doesn’t reduce to that — the two are unrelated
2. Given the diff values are what matters, why does taking the SMALLEST n diffs (not the largest) minimize the total?
- The sum of any n values out of 2n is minimized by choosing the n smallest of them
- Larger diffs always mean cheaper flights
- Which n values are chosen never changes the total
3. What’s the core exchange-argument idea that proves the greedy split is optimal, not just a good guess?
- Swapping any A/B pair that’s out of sorted order never increases (and can only decrease) the total cost
- It was checked against enough LeetCode examples to be confident
- There’s no proof — it’s a heuristic that happens to score well
4. Why is the brute force fundamentally worse than “slow” — what makes it infeasible even for moderate n?
- The number of groupings it checks, C(2n, n), grows combinatorially — not linearly or even polynomially
- It’s only a small constant factor slower than the greedy solution
- It has the same Big O as the greedy solution, just with worse constants
5. What’s the risk of writing costs.sort() instead of costs.sort((a, b) => (a[0]-a[1]) - (b[0]-b[1])) in this solution?
- With no comparator, .sort() converts elements to strings and sorts lexicographically — silently the wrong order for this problem
- It throws a TypeError immediately, so the bug is easy to catch
- .sort() always falls back to numeric ascending order automatically