1. 问题背景与题目解析
LeetCode 765题"情侣牵手"是一个经典的图论问题,表面看似简单却蕴含着精妙的算法思想。题目描述如下:N对情侣坐在连续排列的2N个座位上,想要牵到对方的手。计算最少交换座位的次数,以便每对情侣都能并肩坐在一起。每次交换可选择任意两人,让他们互换座位。
问题的输入是一个长度为2N的数组row,其中row[i]表示坐在第i个座位上的人的ID。情侣的ID是连续的偶数-奇数对,例如(0,1)、(2,3),以此类推。例如对于输入row = [0, 2, 1, 3],需要交换座位2和座位3上的人,最少交换次数为1。
这个问题的核心在于识别出座位排列中形成的"环"结构。将每对座位视为图中的节点,如果两个座位上的人不是情侣但他们的伴侣坐在其他座位上,就形成了一条边。最终这个问题转化为计算图中环的数量,最少交换次数等于总对数减去环的数量。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 并查集解法详解
2.1 并查集基础原理
并查集(Disjoint Set Union)是解决此类连通性问题的利器。它主要支持两种操作:
- Find:查找元素所属的集合代表
- Union:合并两个元素所在的集合
在本题中,我们可以将每个座位对(2i, 2i+1)视为一个集合。初始时,每个集合都是独立的。当我们发现两个座位上的用户是情侣关系时,就将这两个座位所属的集合合并。
并查集的典型实现包括路径压缩和按秩合并两种优化:
java复制class UnionFind {
private int[] parent;
private int[] rank;
public UnionFind(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
}
2.2 应用到情侣牵手问题
对于本题,我们可以将每对情侣视为图中的节点。遍历座位时,对于每两个相邻的座位(2i, 2i+1),我们找到这两个人对应的情侣组:
- 对于座位上的人x,其情侣应该是x^1(利用异或性质)
- 将当前座位对与情侣所在的座位对进行union操作
最终答案就是总座位对数减去连通分量的数量。Java实现如下:
java复制public int minSwapsCouples(int[] row) {
int n = row.length / 2;
UnionFind uf = new UnionFind(n);
for (int i = 0; i < row.length; i += 2) {
int a = row[i] / 2;
int b = row[i + 1] / 2;
uf.union(a, b);
}
int count = 0;
for (int i = 0; i < n; i++) {
if (uf.find(i) == i) {
count++;
}
}
return n - count;
}
3. 贪心算法解法与实现
3.1 贪心算法思路
虽然并查集解法很优雅,但在面试场景下,面试官可能更期望看到直接的贪心解法。贪心算法的核心思想是:从左到右处理每个座位,如果当前人和右边的人不是情侣,就找到其情侣并交换过来。
具体步骤:
- 遍历数组,每次步进2个位置(处理座位对)
- 对于位置i的人x,检查位置i+1的人是否是x的情侣(x^1)
- 如果不是,从i+2开始向后查找x的情侣,找到后与i+1位置交换
- 记录交换次数
3.2 Java实现代码
java复制public int minSwapsCouples(int[] row) {
int swaps = 0;
for (int i = 0; i < row.length; i += 2) {
int x = row[i];
int y = x ^ 1; // x的情侣
if (row[i + 1] == y) {
continue; // 已经是情侣
}
// 需要交换
for (int j = i + 2; j < row.length; j++) {
if (row[j] == y) {
// 交换row[i+1]和row[j]
int temp = row[i + 1];
row[i + 1] = row[j];
row[j] = temp;
swaps++;
break;
}
}
}
return swaps;
}
这个解法的时间复杂度是O(n^2),在最坏情况下需要遍历整个数组来寻找情侣。虽然不如并查集的O(nα(n))高效,但在实际面试中更容易快速实现和理解。
4. 算法优化与复杂度分析
4.1 使用哈希表优化查找
我们可以通过预处理建立位置哈希表来优化贪心算法中的查找过程:
java复制public int minSwapsCouples(int[] row) {
int n = row.length;
int[] pos = new int[n];
for (int i = 0; i < n; i++) {
pos[row[i]] = i; // 记录每个人的位置
}
int swaps = 0;
for (int i = 0; i < n; i += 2) {
int x = row[i];
int y = x ^ 1;
if (row[i + 1] != y) {
int j = pos[y];
// 交换row[i+1]和row[j]
int temp = row[i + 1];
row[i + 1] = row[j];
row[j] = temp;
// 更新位置信息
pos[row[j]] = j;
pos[row[i + 1]] = i + 1;
swaps++;
}
}
return swaps;
}
这种优化将时间复杂度降到了O(n),因为每次查找情侣位置只需要O(1)时间。空间复杂度为O(n)用于存储位置信息。
4.2 复杂度对比
| 算法类型 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 基础贪心 | O(n^2) | O(1) | 小规模数据,实现简单 |
| 哈希优化 | O(n) | O(n) | 通用场景,性能最佳 |
| 并查集 | O(nα(n)) | O(n) | 需要理论证明的场景 |
在实际面试中,建议先提出基础贪心解法,然后分析其不足,再给出优化方案,最后可以提到并查集解法展示算法深度。
5. 边界条件与测试用例
5.1 常见边界情况
- 已经全部配对的输入:如[0,1,2,3],应返回0
- 需要全部交换的输入:如[0,2,1,3],应返回1
- 大规模数据测试:验证算法性能
- 最小输入:如[0,1],应返回0
- 乱序输入:如[3,2,0,1],应返回0(因为3^1=2,0^1=1)
5.2 测试用例设计
java复制@Test
public void testMinSwapsCouples() {
Solution solution = new Solution();
assertEquals(0, solution.minSwapsCouples(new int[]{0,1,2,3})); // 已配对
assertEquals(1, solution.minSwapsCouples(new int[]{0,2,1,3})); // 需要1次交换
assertEquals(2, solution.minSwapsCouples(new int[]{3,2,0,1})); // 需要2次交换
assertEquals(0, solution.minSwapsCouples(new int[]{0,1})); // 最小输入
assertEquals(3, solution.minSwapsCouples(new int[]{5,4,2,6,3,1,0,7})); // 复杂案例
}
6. 实际应用与变种问题
6.1 现实中的应用场景
虽然"情侣牵手"问题看起来有些抽象,但其核心思想在以下场景有实际应用:
- 数据重组:需要将相关数据元素移动到相邻位置
- 内存管理:优化对象在内存中的布局以提高缓存命中率
- 任务调度:将相关联的任务分配到相邻的时间片或处理器
6.2 相关变种问题
- 环形排列:如果座位是环形排列的,解法需要如何调整?
- 加权交换:如果不同位置的交换有不同的成本,如何找到最小成本解?
- 多对关系:如果不是情侣关系而是更复杂的关系网络,如何解决?
- 限制交换位置:如果只能交换相邻座位,如何解决?
例如,对于环形排列变种,我们需要考虑首尾座位也是相邻的,可以在原始算法基础上增加对最后一个座位和第一个座位的检查。
7. 解题心得与面试技巧
7.1 解题思考过程
当我第一次遇到这个问题时,尝试了以下思考路径:
- 观察示例,理解题目要求
- 尝试小规模案例手动模拟
- 发现每次交换可以解决一对情侣的问题,但可能有更优解
- 联想到图论中的环结构
- 尝试用并查集建模连通性
- 验证思路的正确性
7.2 面试回答策略
在面试中回答此类问题时,建议采用以下策略:
- 先澄清问题,确认理解正确
- 给出暴力解法的思路(如回溯)
- 分析暴力解法的问题(复杂度高)
- 提出优化思路(贪心或并查集)
- 讨论算法正确性
- 编写代码并测试
- 分析时间空间复杂度
- 讨论可能的优化和变种
7.3 常见错误与避免方法
- 错误理解情侣ID关系:误以为情侣ID是任意组合。实际上ID为2k和2k+1是一对。
- 贪心算法实现错误:在交换后没有正确更新位置信息,导致后续查找错误。
- 并查集初始化错误:忘记初始化parent数组为自身。
- 边界条件处理不当:没有考虑已经全部配对的情况。
避免这些错误的方法包括:仔细阅读题目描述,多用手动示例验证,编写全面的测试用例。
