N couples sit in 2N seats arranged in a row and want to hold hands. We want to know the minimum number of swaps so that every couple is sitting side by side. A swap consists of choosing any two people, then they stand up and switch seats.
The people and seats are represented by an integer from 0
to 2N-1
, the couples are numbered in order, the first couple being (0, 1)
, the second couple being (2, 3)
, and so on with the last couple being (2N-2, 2N-1)
.
The couples' initial seating is given by row[i]
being the value of the person who is initially sitting in the i-th seat.
Example 1:
Input: row = [0, 2, 1, 3] Output: 1 Explanation: We only need to swap the second (row[1]) and third (row[2]) person.
Example 2:
Input: row = [3, 2, 0, 1] Output: 0 Explanation: All couples are already seated side by side.
Note:
len(row)
is even and in the range of[4, 60]
.row
is guaranteed to be a permutation of0...len(row)-1
.
這道題給了我們一個長度為n的數組,里面包含的數字是 [0, n-1] 范圍內的數字各一個,讓我們通過調換任意兩個數字的位置,使得相鄰的奇偶數靠在一起。因為要兩兩成對,所以題目限定了輸入數組必須是偶數個。我們要明確的是,組成對兒的兩個是從0開始,每兩個一對兒的。比如0和1,2和3,像1和2就不行。而且檢測的時候也是兩個數兩個數的檢測,左右順序無所謂,比如2和3,或者3和2都行。當我們暫時對如何用代碼來解決問題沒啥頭緒的時候,一個很好的辦法是,先手動解決問題,意思是,假設這道題不要求你寫代碼,就讓你按照要求排好序怎么做。我們隨便舉個例子來說吧,比如:
[3 1 4 0 2 5]
我們如何將其重新排序呢?首先明確,我們交換數字位置的動機是要湊對兒,如果我們交換的兩個數字無法組成新對兒,那么這個交換就毫無意義。來手動交換吧,我們兩個兩個的來看數字,前兩個數是3和1,我們知道其不成對兒,數字3的老相好是2,不是1,那么怎么辦呢?我們就把1和2交換位置唄。好,那么現在3和2牽手成功,度假去了,再來看后面的:
[3 2 4 0 1 5]
我們再取兩數字,4和0,互不認識!4跟5有一腿兒,不是0,那么就把0和5,交換一下吧,得到:
[3 2 4 5 1 0]
好了,再取最后兩個數字,1和0,兩口子,不用動!前面都成對的話,最后兩個數字一定成對。而且這種方法所用的交換次數一定是最少的,不要問博主怎么證明,博主也不會|||-.-~明眼人應該已經看出來了,這就是一種貪婪算法Greedy Algorithm。思路有了,代碼就很容易寫了,注意這里在找老伴兒時用了一個trick,一個數‘異或’上1就是其另一個位,這個不難理解,如果是偶數的話,最后位是0,‘異或’上1等於加了1,變成了可以的成對奇數。如果是奇數的話,最后位是1,‘異或’上1后變為了0,變成了可以的成對偶數。參見代碼如下:
解法一:
class Solution { public: int minSwapsCouples(vector<int>& row) { int res = 0, n = row.size(); for (int i = 0; i < n; i += 2) { if (row[i + 1] == (row[i] ^ 1)) continue; ++res; for (int j = i + 1; j < n; ++j) { if (row[j] == (row[i] ^ 1)) { row[j] = row[i + 1]; row[i + 1] = row[i] ^ 1; break; } } } return res; } };
下面我們來看一種使用聯合查找Union Find的解法。該解法對於處理群組問題時非常有效,比如島嶼數量有關的題就經常使用UF解法。核心思想是用一個root數組,每個點開始初始化為不同的值,如果兩個點屬於相同的組,就將其中一個點的root值賦值為另一個點的位置,這樣只要是相同組里的兩點,通過find函數會得到相同的值。 那么如果總共有n個數字,則共有 n/2 對兒,所以我們初始化 n/2 個群組,我們還是每次處理兩個數字。每個數字除以2就是其群組號,那么屬於同一組的兩個數的群組號是相同的,比如2和3,其分別除以2均得到1,所以其組號均為1。那么這對解題有啥作用呢?作用忒大了,由於我們每次取的是兩個數,且計算其群組號,並調用find函數,那么如果這兩個數的群組號相同,那么find函數必然會返回同樣的值,我們不用做什么額外動作,因為本身就是一對兒。如果兩個數不是一對兒,那么其群組號必然不同,在二者沒有歸為一組之前,調用find函數返回的值就不同,此時我們將二者歸為一組,並且cnt自減1,忘說了,cnt初始化為總群組數,即 n/2。那么最終cnt減少的個數就是交換的步數,還是用上面講解中的例子來說明吧:
[3 1 4 0 2 5]
最開始的群組關系是:
群組0:0,1
群組1:2,3
群組2:4,5
取出前兩個數字3和1,其群組號分別為1和0,帶入find函數返回不同值,則此時將群組0和群組1鏈接起來,變成一個群組,則此時只有兩個群組了,cnt自減1,變為了2。
群組0 & 1:0,1,2,3
群組2:4,5
此時取出4和0,其群組號分別為2和0,帶入find函數返回不同值,則此時將群組0&1和群組2鏈接起來,變成一個超大群組,cnt自減1,變為了1。
群組0 & 1 & 2:0,1,2,3,4,5
此時取出最后兩個數2和5,其群組號分別為1和2,因為此時都是一個大組內的了,帶入find函數返回相同的值,不做任何處理。最終交換的步數就是cnt減少值,為2,參見代碼如下:
解法二:
class Solution { public: int minSwapsCouples(vector<int>& row) { int res = 0, n = row.size(), cnt = n / 2; vector<int> root(n, 0); for (int i = 0; i < n; ++i) root[i] = i; for (int i = 0; i < n; i += 2) { int x = find(root, row[i] / 2); int y = find(root, row[i + 1] / 2); if (x != y) { root[x] = y; --cnt; } } return n / 2 - cnt; } int find(vector<int>& root, int i) { return (i == root[i]) ? i : find(root, root[i]); } };
下面這種使用HashMap的解法,本質其實也是聯合查找Union Find。我們知道只有群組里面是數字,才能使用root數組,有些非數字的情況,比如字符串,就要使用HashMap了,當然數字也是可以使用HashMap的。我們這里的helper子函數相當於同時包括了鏈接群組和find查找兩部分,在主函數中,我們還是兩個兩個處理,並且把群組號帶入helper函數,在helper函數中,我們將較小數和較大數區分出來,如果二者相同,表明是同一個群組的,不做任何處理,直接返回。否則的話,建立二者的映射,這就是上面解法中的鏈接群組操作,這樣看出來了吧,二者的本質其實是一樣的,參見代碼如下:
解法三:
class Solution { public: int minSwapsCouples(vector<int>& row) { unordered_map<int, int> m; for (int i = 0; i < row.size(); i += 2) { helper(m, row[i] / 2, row[i + 1] / 2); } return m.size(); } void helper(unordered_map<int, int>& m, int x, int y) { int c1 = min(x, y), c2 = max(x, y); if (c1 == c2) return; if (m.count(c1)) helper(m, m[c1], c2); else m[c1] = c2; } };
這道題的一個Follow up就是fun4LeetCode大神的帖子中討論的N整數問題 N Integers Problems,簡單來說就是最少使用幾步可以將所有的數字移回其正確位置,比如數組 [0 3 1 2] 變回 [0 1 2 3] 需要幾步,兩步就夠了,先交換3和2,變成 [0 2 1 3],再交換2和1,變回 [0 1 2 3]。怎么做呢?實際上在遍歷某一個位置i,如果發現 i != rows[i],我們就不同的通過交換i和rows[i],然后讓 row[i] 等於 row[row[i]],使其最終相等,是不是也有點Union Find的影子在里面呢?真是很有趣呢~面白以~
類似題目:
參考資料:
https://leetcode.com/problems/couples-holding-hands/solution/