LeetCode 1029. 两地调度
题目描述
题意分析
有
2n个人,costs[i] = [aCost, bCost]表示第i个人飞 A 市和飞 B 市各要花多少钱。要求恰好n个人去 A、n个人去 B,求最小总费用。关键约束是「两边人数必须严格相等」。如果没有这条,每个人各自选便宜的一边即可,题目就不成立了。正是这个名额限制把「独立的
2n个选择」耦合成了一个整体决策:让某人去 A,就意味着挤掉了另一个人去 A 的名额。由此引出正确的度量方式。直接比较
aCost和bCost的绝对大小没有意义——真正要比的是「这个人改去 A 而不是 B,会额外多花(或少花)多少钱」,也就是差值aCost - bCost。差值越小(越负),把 A 的名额给他越划算。这一步把二维的[aCost, bCost]降成了一维的可排序键,是全题的转折点。约束是
costs.length == 2n、1 ≤ n ≤ 100、费用不超过 1000,规模极小,$O(n \log n)$ 的排序绰绰有余;总费用最大约200 × 1000 = 2 × 10^5,int完全够用。边界:
n可以是 1(两个人,一人一边);两个人的差值可能相等,此时谁去哪边都不影响总额,排序是否稳定无所谓。
解法:按差值排序
核心思路
先假设所有人都去 B,总费用为
sumB。把第i个人改派到 A,会让总费用变化:
diff[i] = costs[i][0] - costs[i][1]现在必须从
2n个人中恰好改派n人,目标变成让选中的n个差值之和最小。因此按diff升序排序,前n人去 A,其余人去 B 即可。差值为负表示改派到 A 还能省钱,差值越小,A 的名额越应该优先给他。交换论证:若一个方案让差值较大的
j去 A、差值较小的i去 B,且diff[i] <= diff[j],交换两人的城市后人数不变,总费用变化为diff[i] - diff[j] <= 0。不断消除这样的逆序对,最终就得到「排序后前半去 A、后半去 B」的方案,且费用不会增加,因此该方案最优。代码直接按排序后的位置累加实际费用,不必显式计算
sumB。排序会改变costs的顺序,这是本解法唯一的输入副作用。
解题步骤
- 对每个人计算排序键
A 费用 - B 费用,按该键升序原地排序。- 令
n = costs.length / 2。- 排序后的前
n人累加 A 费用,后n人累加 B 费用。样例
[[10,20],[30,200],[400,50],[30,20]]的差值为[-10,-170,350,10]。排序后前两人去 A 花30 + 10,后两人去 B 花20 + 50,总费用为110。反例
[[5,10],[6,1],[7,20],[8,2]]说明不能按 A 费用排序:按差值选人得到7 + 5 + 1 + 2 = 15;按 A 费用选前两人去 A 会得到5 + 6 + 20 + 2 = 33。
代码实现
import java.util.Arrays;
class Solution {
public int twoCitySchedCost(int[][] costs) {
Arrays.sort(costs, (a, b) -> Long.compare(
(long) a[0] - a[1], (long) b[0] - b[1]));
int n = costs.length / 2;
int answer = 0;
for (int i = 0; i < n; i++) {
answer += costs[i][0] + costs[i + n][1];
}
return answer;
}
}
import "sort"
func twoCitySchedCost(costs [][]int) int {
sort.Slice(costs, func(i, j int) bool {
left := int64(costs[i][0]) - int64(costs[i][1])
right := int64(costs[j][0]) - int64(costs[j][1])
return left < right
})
n := len(costs) / 2
answer := 0
for i := 0; i < n; i++ {
answer += costs[i][0] + costs[i+n][1]
}
return answer
}
复杂度分析
设总人数为 $N$。
- 时间复杂度:$O(N \log N)$,排序占主导;求和为 $O(N)$。
- 空间复杂度:算法自身只用 $O(1)$ 标量,但还需计入标准库排序开销。Java 对对象数组排序最坏需要 $O(N)$ 辅助空间;Go 的
sort.Slice额外栈空间为 $O(\log N)$。
关键点总结
- 固定名额的二选一问题,应比较两个选择的相对代价,而不是任一边的绝对费用。
- 「全部去 B,再改派
n人」把问题化为选择最小的n个增量。- 交换论证同时保证人数约束不变,并证明所有逆序安排都不优于差值顺序。
- 相同差值的人如何排序不影响答案;实现无需依赖稳定排序。
易错点总结
- 按 A 费用或 B 费用排序:绝对费用没有表达「改派」的机会成本,上面的反例会得到 33 而非 15。
- 把差值按降序排却仍让前半去 A:样例会得到 650,而正确答案是 110。
- 每个人独立选择较便宜的城市:
[[1,100],[2,100],[3,100],[4,100]]会把 4 人都送往 A,违反各n人的约束。- 排序后仍对每个人取
min(A,B):[[1,2],[1,2],[1,2],[1,2]]会得到非法答案 4,合法最优值是 6。- Go 排序比较函数必须使用严格的
<;相等时也返回 true 会破坏比较器要求。- 比较器里直接做 32 位减法在本题约束下不会溢出,但模板迁移到更大费用范围时会出错;代码先提升到 64 位再求差,避免比较器因溢出破坏顺序。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 1005. K 次取反后最大化的数组和 | 简单 | 同为排序后取前缀的贪心,但排序键是原值、且要额外处理剩余次数的奇偶性 |
| 455. 分发饼干 | 简单 | 两个数组各自排序后双指针匹配,贪心对象是「配对」而非「二选一」 |
| 881. 救生艇 | 中等 | 排序后头尾双指针,每步要同时看最轻与最重两端,约束是每船至多两人 |
| 502. IPO | 困难 | 名额固定为 k 次,但候选集合随资本增长而动态解锁,需要排序配合大顶堆 |
| 630. 课程表 III | 困难 | 按截止时间排序后反悔式贪心,选进来的元素还能被更优的替换掉 |
| 1235. 规划兼职工作 | 困难 | 排序只是预处理,真正的最优化必须靠 DP 加二分,用来对照「何时排序就够、何时不够」 |
| 1353. 最多可以参加的会议数目 | 中等 | 同样是名额受限的分配,但每个时间点只有一个名额,需要用小顶堆按截止时间贪心 |