[LeetCode] Couples Holding Hands 兩兩握手


 

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:

  1. len(row) is even and in the range of [4, 60].
  2. row is guaranteed to be a permutation of 0...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的影子在里面呢?真是很有趣呢~面白以~

 

類似題目:

Missing Number

First Missing Positive

 

參考資料:

https://leetcode.com/problems/couples-holding-hands/solution/

https://leetcode.com/problems/couples-holding-hands/discuss/113353/Monster-Style-C++-O(n)-unordered_map

https://leetcode.com/problems/couples-holding-hands/discuss/113362/JavaC++-O(N)-solution-using-cyclic-swapping

https://leetcode.com/problems/couples-holding-hands/discuss/117520/Java-union-find-easy-to-understand-5-ms

 

LeetCode All in One 題目講解匯總(持續更新中...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM