765 情侣牵手
- 链接:https://leetcode.cn/problems/couples-holding-hands/
- 难度:困难
- 标签:贪心、并查集
题目
n n n 对情侣坐在连续排列的 2 n 2n 2n 个座位上,想要牵到对方的手。
人和座位由一个整数数组 r o w row row 表示,其中 r o w [ i ] row[i] row[i] 是坐在第 i i i 个座位上的人的 ID。情侣们按顺序编号,第一对是 (0, 1),第二对是 (2, 3),以此类推,最后一对是 (2n-2, 2n-1)。
返回 最少交换座位的次数,以便每对情侣可以并肩坐在一起。 每次交换可选择任意两人,让他们站起来交换座位。
Solution:贪心
Explanation
要想让 n n n对情侣互相牵手,那么必须按照要求调换一部分人的次序,使所有的情侣可以互相牵手。要想让交换座位的次数最少,那么可以考虑一种做法:从前向后两两遍历数组,假设当前遍历到了 a a a 和 b b b ,如果他们互为情侣,则跳过;如果不是情侣,则在后面的未遍历到的部分中找到 a a a 的伴侣,并将其与 b b b 进行交换。这种做法保证了“按需调换”,也就是只有当需要进行交换时才进行交换,此时的总的交换次数最少。但是贪心是一种比较想当然的做法,我们需要进行一些证明验证它的正确性。
证明
假设我们已经处理好了前 k - 1 对情侣,现在处理第 k 对位置上的人。有两种情况:
- 他们不是情侣:此时,我们在寻找第一个人的情侣,并且交换他和第二个人,这样第 k 对位置上就是情侣了,而且因为前 k - 1对情侣已经处理完毕了,所以第一个人的情侣必定是在后面,所以交换他们不会影响到已经处理好的情侣,因此只需要交换一次就可以使第 k 对位置上的人为情侣,这是最少的交换次数。
- 他们是情侣:那么不需要交换即可。
- 在处理完第 k 对位置后,继续处理第 k + 1 对位置,这个和处理第 k 对位置的方式完全一致,是一个规模更小的子问题。因此可以这样做。
小技巧
交换情侣时需要找到对应情侣的位置,才可以进行交换,此时有两个问题:
- 如何确定另一个情侣的编号?
- 可以通过异或操作。例如(2,3),2 ^ 1 = 3, 3 ^ 1 = 2。那么第 i i i 位的另一个情侣的编号为 row[i] ^ 1
- 如何寻找另一个情侣的位置?
- 朴素的做法是遍历,缺点是时间复杂度高,每次是 O ( n ) O(n) O(n)
- 可以先使用哈希表对值与下标进行预处理,避免使用for循环以降低时间复杂度,每次时间复杂度降为 O ( 1 ) O(1) O(1)
- 寻找到对应情侣后,注意也要交换他们在哈希表中的值。比如 h[4] = 4, h[8] = 9; 现在需交换他们,需要 h[4] = 9, f[8] = 4
Code
class Solution {
public:
int minSwapsCouples(vector<int>& row) {
int n = row.size();
int ans = 0;
unordered_map<int, int> hm;
for (int i = 0; i < n; ++i)
hm[row[i]] = i;
for (int i = 0; i < n; i += 2) {
if (row[i] != (row[i + 1] ^ 1)) {
ans++;
int t = hm[row[i] ^ 1];
hm[row[i] ^ 1] = i + 1;
hm[row[i + 1]] = t;
swap(row[i + 1], row[t]);
}
}
return ans;
}
};
Time Complexity And Space Complexity
- ⌛️时间复杂度: O ( n ) O(n) O(n)
- 🌎空间复杂度: O ( n ) O(n) O(n)
Solution:并查集
Explanation
- 需要知道一点:两对情侣坐错需要交换一次;三对情侣做错,需要交换两次;四队情侣坐错,需要交换三次;也就是说,n 对情侣坐错,需要交换 n - 1 次。
- 假设有 2n 个人,那么就有 n 对情侣。我们根据情侣编号作为并查集的依据,比如某对情侣的各自编号为 8,9:那么此对情侣的编号就为 4:计算方法为 8 / 2 = 9 / 2 = 4. 也就是情侣编号为自己的值 / 2,这样每对情侣中的两个人计算得到的编号是一致的。
- 我们就可以将每对位置上人按照他们对应的情侣编号合并起来,这样每个连通块有几个元素就代表有几对情侣互相坐错,最终答案就是每个连通块的元素个数减一之和。
Code
class Solution {
public:
int minSwapsCouples(vector<int>& row) {
int n = row.size(), m = n / 2;
vector<int> fa(m);
iota(fa.begin(), fa.end(), 0);
vector<int> cnt(m, 1);
for (int i = 0; i < n; i += 2)
merge(row[i] / 2, row[i + 1] / 2, fa, cnt);
unordered_set<int> hs;
int ans = 0;
for (int i = 0; i < n; i += 2) {
int fi = find(row[i] / 2, fa);
if (hs.count(fi) == 0) {
ans += cnt[fi] - 1;
hs.insert(fi);
}
}
return ans;
}
void merge(int x, int y, vector<int>& fa, vector<int>& cnt) {
int fx = find(x, fa);
int fy = find(y, fa);
if (fx == fy)
return;
fa[fy] = fx;
cnt[fx] += cnt[fy];
}
int find(int x, vector<int>& fa) {
return x == fa[x] ? x : fa[x] = find(fa[x], fa);
}
};
Time Complexity And Space Complexity
- ⌛️时间复杂度: O ( N l o g N ) O(NlogN) O(NlogN),其中 N N N 为情侣的总数。这里的并查集使用了路径压缩,但是没有使用按秩合并,最坏情况下的时间复杂度是 O ( N log N ) O(N \log N) O(NlogN),平均情况下的时间复杂度依然是 O ( N α ( N ) ) O(N \alpha (N)) O(Nα(N)),其中 α \alpha α 为阿克曼函数的反函数, α ( N ) \alpha (N) α(N) 可以认为是一个很小的常数。
- 🌎空间复杂度: O ( n ) O(n) O(n)