[leetcode] 並查集(Ⅲ)

sinkinben發表於2020-05-31

嬰兒名字

題目[Interview-1707]:典型並查集題目。

解題思路

首先對 names 這種傻 X 字串結構進行預處理,轉換為一個 mapkey 是名字,val 是名字出現的次數。

然後是把 synonyms 轉換為並查集結構,需要注意的是:總是把字典序較小的名字作為連通分量的根。

最後以連通分量的根作為代表,計算每個連通分量的總權重(即每個名字的次數之和)。

程式碼實現

class Solution
{
public:
    unordered_map<string, string> root;
    vector<string> trulyMostPopular(vector<string> &names, vector<string> &synonyms)
    {
        vector<string> ans;
        unordered_map<string, int> table;
        for (auto &s : names)
        {
            int idx = s.find('(');
            string n = s.substr(0, idx);
            int val = stoi(s.substr(idx + 1, s.length() - 2 - idx));
            table[n] = val;
        }
        // build the disjoint-union set
        for (auto &str : synonyms)
        {
            int idx = str.find(',');
            string n1 = str.substr(1, idx - 1);
            string n2 = str.substr(idx + 1, str.length() - 2 - idx);
            merge(n1, n2);
        }
        // calculate the frequency of root nodes
        unordered_map<string, int> mapAns;
        for (auto &p : table)
            mapAns[find(p.first)] += p.second;

        for (auto &p : mapAns)
        {
            string s = p.first + "(" + to_string(p.second) + ")";
            ans.emplace_back(s);
        }

        return ans;
    }
    string find(string x)
    {
        return root.count(x) == 0 ? (x) : (root[x] = find(root[x]));
    }
    void merge(string x, string y)
    {
        x = find(x), y = find(y);
        if (x < y)
            root[y] = x;
        else if (x > y)
            root[x] = y;
        // do nothing if x == y
    }
};

冗餘連線 Ⅱ

題目[685]:?需要觀察出一點 trick .

解題思路

請先完成這篇文章中的 “冗餘連線”

看討論區的題解,本題需要分成 2 種情況討論(沒想到真做不出來?):

  • 存在入度為 2 的點

    即給出的示例 1 。由於本題的隱含條件是:去除一邊後所得的圖是樹(即每個點的入度均為 1 ),所以可以確定這種情況下有且只有一個入度為 2 的點(通過反證法易證)。

    設入度為 2 的點為 t ,先在 edges 中剔除以 t 為終點的邊(有且僅有 2 個這樣的邊),建立並查集結構。

    設被剔除的 2 個邊先後分別為 e1e2,那麼嘗試在並查集中加入 e1 ,如果加入 e1 後依然無環,說明 e1 屬於樹的邊,那麼返回 e2 ;如果加入 e1 後有環,說明該剔除 e1 ,即返回 e1 。(這麼做的原因是題目要求:如果有多個滿足則返回最後一個。)

  • 所有的點入度為 1

    即給出的示例 2 。這種情況下,必然存在一個邊 e 使得圖中存在一個有向環,該邊 e 即為所求。掃描 edges 的每一個邊,同時建立並查集結構,如果某一邊使得 find(x) == y,說明有環,返回該邊即可。

程式碼實現

class Solution
{
public:
    vector<int> root;
    vector<int> findRedundantDirectedConnection(vector<vector<int>> &edges)
    {
        root.resize(edges.size() + 2, -1);
        // get node whose indegree is 2
        int t = checkIndegree(edges);
        // if there is not such node in graph
        if (t == -1)
        {
            for (auto &v : edges)
            {
                int x = v[0], y = v[1];
                if (find(x) == y)
                    return v;
                merge(x, y);
            }
        }
        else
        {
            vector<vector<int>> candidate;
            for (auto &v : edges)
            {
                if (v[1] == t)
                    candidate.emplace_back(v);
                else
                    merge(v[0], v[1]);
            }
            assert(candidate.size() == 2);
            auto &v = candidate[0];
            int a = find(v[0]), b = find(v[1]);
            if (find(a) != b)
                return candidate[1];
            return v;
        }
        // should not be here
        return vector<int>();
    }

    int checkIndegree(vector<vector<int>> &egdes)
    {
        unordered_map<int, int> m;
        for (auto &v : egdes)
            m[v[1]]++;
        for (auto &p : m)
            if (p.second >= 2)
                return p.first;
        return -1;
    }
    int find(int x)
    {
        return root[x] == -1 ? (x) : (root[x] = find(root[x]));
    }
    void merge(int x, int y)
    {
        x = find(x), y = find(y);
        if (x != y)
            root[y] = x;
    }
};

賬戶合併

題目[721]:一般難度題目?,有多種解法。

解題思路

  • DFS 解法

建立一個 map 記錄每一個 email 對應的主人的 name ,方便後續的結果處理。

把所有的 email 建立一個圖(以鄰接表的形式)。每一個 account 的 email 都是相鄰的,對於以下的 account :

account1 = ["John", "e1", "e2", "e3"]
account2 = ["John", "e4", "e3", "e5", "e2"]

在圖 graph 中表現為:

      _____________
      |           |
e1 -- e2 -- e3 -- e5
            |
            e4

其鄰接表為:

e1: [e2]
e2: [e1, e3, e5]
e3: [e2, e4, e5]
e4: [e3]
e5: [e3, e2]

最後,graph 圖中的每一個連通分量必然是屬於同一個人的,使用 DFS 或者 BFS 遍歷整個圖即可。

程式碼實現如下:

class Solution
{
public:
    unordered_map<string, string> mailName;
    unordered_map<string, vector<string>> graph;
    unordered_set<string> visited;	// dfs helper
    vector<vector<string>> accountsMerge(vector<vector<string>> &accounts)
    {
        for (auto &v : accounts)
        {
            string &name = v[0];
            int size = v.size();
            // connect the emails
            for (int i = 1; i < size - 1; i++)
            {
                mailName[v[i]] = name;
                graph[v[i]].emplace_back(v[i + 1]);
                graph[v[i + 1]].emplace_back(v[i]);
            }
            // if this account has only one email, put it into the graph, too 
            if (size == 2 && graph.find(v[1]) == graph.end())
                graph[v[1]] = vector<string>();
            // the last email
            mailName[v[size - 1]] = name;
        }

        vector<vector<string>> ans;
        for (auto &p : graph)
        {
            auto &now = p.first;
            auto &list = p.second;
            vector<string> data({mailName[now]});
            if (visited.count(now) == 0)
            {
                dfs(data, now, list);
                sort(data.begin() + 1, data.end());
                ans.emplace_back(data);
            }
        }
        return ans;
    }
    void dfs(vector<string> &data, const string &now, vector<string> &list)
    {
        visited.insert(now);
        data.emplace_back(now);
        for (auto &x : list)
        {
            if (visited.count(x) == 0)
            {
                dfs(data, x, graph[x]);
            }
        }
    }
};
  • 並查集解法

其實與上面差不多 ? . 只不過圖的結構換成並查集,查詢連通分量就可以不用 DFS 搜尋了。

對於每個 account 的 email 列表,把第一個作為連通分量的根,把從第二個開始及後面的所有 email ,都將它們與第一個合併。

class Solution
{
public:
    unordered_map<string, string> root;
    unordered_map<string, string> emailName;
    vector<vector<string>> accountsMerge(vector<vector<string>> &accounts)
    {
        // 建立並查集(結果不是完全路徑壓縮的)
        for (auto &v : accounts)
        {
            string &name = v.front();
            int size = v.size();
            emailName[v[1]] = name;
            for (int i = 2; i < size; i++)
            {
                emailName[v[i]] = name;
                merge(v[1], v[i]);
            }
        }
        vector<vector<string>> ans;
        unordered_map<string, vector<string>> table;
        // 把同一個連通分量歸類在一起,key是該連通分量的根,val是連通分量的所有節點
        for (auto &p : emailName)
        {
            auto email = p.first, name = p.second;
            table[find(email)].emplace_back(email);
        }
        // 一個分量就是結果中的一個 account
        for (auto &p : table)
        {
            auto v = p.second;
            sort(v.begin(), v.end());
            v.insert(v.begin(), emailName[p.first]);
            ans.emplace_back(v);
        }
        return ans;
    }
    string find(const string &x)
    {
        return root.count(x) == 0 ? (x) : (root[x] = find(root[x]));
    }
    void merge(string x, string y)
    {
        x = find(x), y = find(y);
        if (x != y)
            root[y] = x;
    }
};

相似字串組

題目[839]:?一道 Hard 題目,沒想到暴力解法也能過。

解題思路

每個字串相當於一個節點,以該串在陣列中的下標作為節點記號。

實現 similar 函式,判定 2 個字串是否相似。對所有字串進行兩兩比較(暴力列舉所有情況),判斷是否相似,若相似則在並查集中合併。最後連通分量的個數就是答案。

?執行用時只超過 25% (能用就行,能用就行,又不是不能用.jpg )。

程式碼實現

class Solution
{
public:
    vector<int> root;
    int numSimilarGroups(vector<string> &A)
    {
        int N = A.size();
        root.resize(N, -1);

        for (int i = 0; i < N; i++)
        {
            for (int j = i + 1; j < N; j++)
            {
                if (similar(A[i], A[j]))
                    merge(i, j);
            }
        }
        return count(root.begin(), root.end(), -1);
    }
    int find(int x)
    {
        return root[x] == -1 ? (x) : (root[x] = find(root[x]));
    }
    void merge(int x, int y)
    {
        x = find(x), y = find(y);
        if (x != y)
            root[y] = x;
    }
    bool similar(const string &a, const string &b)
    {
        int len = min(a.length(), b.length());
        int diff = 0;
        for (int i = 0; i < len; i++)
            diff += (a[i] != b[i]);
        return diff <= 2;
    }
};

交換字串中的元素

題目[1202]:?看題解,看題解。

解題思路

?看這個無敵簡潔的題解。

s = "dcabfge", pairs = [[0,3],[1,2],[0,2],[4,6]] 為例進行分析。

首先需要想到一點 trick ,怎麼套上並查集的模板:此處的「交換」是具有傳遞性和對稱性的,因此在 pairs 中,[0,1,2,3] 是可以兩兩進行任意交換(且不限次數),因此每一個 pair = [a,b] 實際上就是並查集中的一個邊,無腦對 pairs 套上並查集的結構進行處理。

此處,並查集的結果是得到 3 個連通分量 [5], [0,1,2,3][4,6] ,(按順序)對應可交換的字元是 [g][d,c,a,b][f,e],要求字典序最小,因此排序結果為 [g], [a,b,c,d][e,f]

將其還原到原始位置:

index: 5 | 0 1 2 3 | 4 6   
chars: g | a b c d | e f
==>
return s = "abcdegf"

程式碼實現

  • 使用交換排序,超時
class Solution
{
public:
    vector<int> root;
    string smallestStringWithSwaps(string s, vector<vector<int>> &pairs)
    {
        int len = s.length();
        if (len == 0)
            return s;
        root.resize(len, -1);
        for (auto &v : pairs)
            merge(v[0], v[1]);
        // extract each component nodes
        // table[r] = [...]
        // r is the root of connected component
        // [...] includes all nodes of the component
        unordered_map<int, vector<int>> table;
        for (int i = 0; i < len; i++)
            table[find(i)].emplace_back(i);
        for (auto &p : table)
        {
            auto &list = p.second;
            sortByValue(list, s);
        }
        return s;
    }
    void sortByValue(vector<int> &list, string &s)
    {
        int n = list.size();
        for (int i = 0; i < n; i++)
        {
            for (int j = i + 1; j < n; j++)
            {
                if (s[list[j]] < s[list[i]])
                    swap(s[list[i]], s[list[j]]);
            }
        }
    }
    // function 'merge' and 'find' are omitted since the space is limited
};
  • 使用 STL 自帶的排序 sort,但執行用時只超過 50%
class Solution
{
public:
    vector<int> root;
    string smallestStringWithSwaps(string s, vector<vector<int>> &pairs)
    {
        int len = s.length();
        if (len == 0)
            return s;
        root.resize(len, -1);

        for (auto &v : pairs)
            merge(v[0], v[1]);

        // extract each component nodes
        unordered_map<int, vector<int>> table;
        for (int i = 0; i < len; i++)
            table[find(i)].emplace_back(i);

        for (auto &p : table)
        {
            string chars = "";
            for (int i : p.second)
                chars.push_back(s[i]);
            sort(chars.begin(), chars.end());
            int j = 0;
            for (int i : p.second)
                s[i] = chars[j++];
        }
        return s;
    }
    // function 'merge' and 'find' are omitted since the space is limited
};

按公因數計算最大元件大小

題目[952]:?刷題?刷題解罷了。

  • 暴力列舉(當然是超時了)

把 num 所在陣列 A 中的下標作為並查集中節點的標號。如果 GCD(A[i] A[j]) > 1 說明節點 ij 可以合併。

class Solution
{
public:
    vector<int> root;
    vector<int> size;
    int largestComponentSize(vector<int> &A)
    {
        int n = A.size();
        root.resize(n, -1);
        size.resize(n, 1);
        for (int i = 0; i < n; i++)
            for (int j = i + 1; j < n; j++)
                if (GCD(A[i], A[j]) > 1)
                    merge(i, j);
        int maxval = size[0];
        for (int x : size)	maxval = max(x, maxval);
        return maxval;
    }
    // function 'find' is omitted
    void merge(int x, int y)
    {
        x = find(x), y = find(y);
        if (x != y)  root[y] = x, size[x] += size[y];
    }
    // a>b is required
    int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
    int GCD(int a, int b)
    {
        if (a > b)	return gcd(a, b);
        else        return gcd(b, a);
    }
};

此處,把 A[i] 的值作為並查集 root 節點的標號(不是下標作為標號)。

對於每一個 A[i] 找出它的所有大於 1 的因子,顯然 A[i] 與這些因子之間是可以合併的,預設把 A[i] 作為連通分量的根(大數值作為根)。那麼,並查集的結果就包含了任意兩個 A[i], A[j] 之間的關係(這裡的關係是指它們是否連通,即是否具有大於 1 的公因子)。為什麼呢?來看個例子。

A = [2, 3, 15, 10]
對於 2 和 3 ,不執行 merge 操作。
對於 15 :merge(15, 3), merge(15, 5)
對於 10 :merge(10, 2), merge(10, 5)

並查集最終結果,每一行表示一個連通分量,行首是連通分量的根:
2 : [2]
3 : [3]
15: [15,3,5,10,2]

顯然,10 和 15 就通過 merge(10,5)merge(15, 5) 這 2 個操作合併到一塊。

最後的問題是如何找出 A 中最大的連通分量?注意,這裡並不是並查集 root 的最大連通分量,因為 rootA 來說是一個擴充後的結構,它包含了 A 中沒有的數值。

新建一個 vector<int> countercounter[r] 表示以 r 為根的連通分量中,陣列 A 中的元素在該分量中出現的次數。 即:

for (int x : A)
    ++counter[find(x)];

對於上述例子:

counter[2] = 1
counter[3] = 1
counter[15] = 2

最終,counter 的最大值即為答案。

程式碼如下:

class Solution
{
public:
    vector<int> root;
    int largestComponentSize(vector<int> &A)
    {
        int maxval = -1;
        for (int x : A)
            maxval = max(maxval, x);
        root.resize(maxval + 1, -1);

        for (int x : A)
        {
            int limit = (int)sqrt(x) + 1;
            for (int i = 2; i < limit; i++)
                if (x % i == 0)
                    merge(x, i), merge(x, x / i);
        }

        vector<int> counter(maxval + 1, 0);
        int ans = -1;
        for (int x : A)
            ans = max(ans, ++counter[find(x)]);
        return ans;
    }
    int find(int x) { return root[x] == -1 ? x : (root[x] = find(root[x])); }
    void merge(int x, int y)
    {
        x = find(x), y = find(y);
        if (x != y) root[y] = x;
    }
};

總結

磕磕碰碰總算把「並查集」的題目刷了一遍,好像還有幾道題是沒做出來的(當然是「下次再努力」啊?)。

相關文章