1. 程式人生 > >並查集演算法題

並查集演算法題

簡要介紹:

並查集:

  一種資料結構,將一個集合分為不相交的子集,在新增新的子集,合併現有子集,判斷元素所在集合時時間複雜度接近常數級。常用於求連通子圖和最小生成樹的Kruskal演算法。

操作:

  makeSet:  初始化,給每個元素分配一個特定的id,以及一個指向自己的指標,表示每個元素都在一個大小為1的集合當中。

  find:       查詢某個元素的根元素。當兩個元素擁有同樣的根元素時,說明他們在同一個集合當中。為了使得時間複雜度接近常數級,在查詢的過程中,可以更新指標指向根元素(程式碼中使用遞迴方法),有人稱其為路徑壓縮。

  Union:      滿足條件則合併兩個子集。如果沒有特殊要求,將一個集合的根指向另一個集合的根即可。也可根據秩或者集合的大小來指定。

leetcode 128

 思路: 使用並查集,比較兩個數,如果他們的差為1,就進行合併。然後在將樹a的根指向樹b的根時(合併操作),將樹a中的節點數新增到樹b的節點數,最後從中找出最大的節點數即可。但這裡忽略了一個問題,如果陣列中的數存在重複,那麼最終得到的數就會偏大。所以需要進行一下預處理。我們只需要給每個數一個唯一標識,然後判斷出兩個標識是否需要合併即可。

程式碼如下:

class Union_Set {
private:
    int size;
    int next[10000];
    int child[10000];
public:
    Union_Set(int size) {
        this->size = size;
    }
    void makeSet() {
        for(int i = 0; i < size; i++) {
            next[i] = i;
            child[i] = 1;
        }
    }
    int find(int i) {
        if(i != next[i])
            next[i] = find(next[i]);
        return next[i];
    }
    void Union(int i, int j) {
        int a = find(i);
        int b = find(j);
        if(a != b) {
            if(child[a] < child[b]) {
                next[i] = b;
                child[b] += child[a];
            } else {
                next[j] = a;
                child[a] += child[b];
            }
        }
    }
    int max_child() {
        int max = INT_MIN;
        for(int i = 0; i < size; i++)
            max = max<child[i]?child[i]:max;
        return max;
    }
};

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        map<int,int> m;
        int size = nums.size();
        for(int i = 0; i < size; i++)
            m.insert(pair<int,int>(nums[i],i));
        if(size == 0)
            return 0;
        Union_Set s(size);
        s.makeSet();
        for(auto&i:m) {
        	auto a = m.find(i.first-1);
        	auto b = m.find(i.first+1);
        	if(a != m.end())
        		s.Union(m[i.first-1],m[i.first]);
        	if(b != m.end())
        		s.Union(m[i.first+1],m[i.first]);
		}
        return s.max_child();
    }
};

leetcode 778

思路:

首先想到是用BFS,對於給定的t執行BFS,如果能訪問到終點,說明t滿足條件。為了減少BFS的次數,可以採用二分的辦法,選取指定的t。程式碼如下:

class Solution {
public:
    int swimInWater(vector<vector<int>>& grid) {
        int top = INT_MIN;
        if(grid.size() == 0)
            return 0;
        int height = grid.size();
        int width = grid[0].size();
        for(int i = 0; i < height; i++)
        for(int j = 0; j < width; j++) {
            top = max(top, grid[i][j]);
        }
        pair<int,int> end = make_pair(height-1,width-1);
        int low = grid[0][0];
        int x_diff[4] = {0,0,1,-1};
        int y_diff[4] = {1,-1,0,0};
        while(1) {
            set<pair<int,int>> visited = {make_pair(0,0)};
            queue<pair<int,int>> q;
            q.push(make_pair(0,0));
            int mid = (low+top)/2;
            int s = q.size();
            while(!q.empty()){
                pair<int,int> tmp = q.front();
                q.pop();
                for(int i = 0; i < 4; i++) {
                    if(tmp.first+x_diff[i] < 0 || tmp.first+x_diff[i] >= height ||
                       tmp.second+y_diff[i] <0 || tmp.second+y_diff[i] >= width)
                       continue;
                    if(grid[tmp.first+x_diff[i]][tmp.second+y_diff[i]] <= mid) {
                        pair<int,int> n = pair<int,int>(tmp.first+x_diff[i], tmp.second+y_diff[i]);
                        if(visited.find(n) == visited.end()) {
                            visited.insert(n);
                            q.push(n);
                        }                          
                    }
                }
            }    
            if(visited.find(end) != visited.end()) {
                top = mid;
                if(top == low)
                    return top;
            } else {
                if(mid == low)
                    return top;
                low = mid;
            }
        }
    }
};

 需要注意的是,二維矩陣的座標原點位於左上角,(x,y)中的x實際是y軸的距離,y實際是x軸的距離(又有點生疏了),然後二分到最後的時候有兩種情況,如果最後的mid不滿足條件,那麼low和top將一直保持為low=mid, top=mid+1,此時應該選擇top為答案。而如果最後的mid滿足條件,top和low將合併,也選擇top為答案即可。

但這個演算法複雜度有點高,只beat 10%+。那麼再來考慮下使用並查集。

並查集的結構不變,在合併時選擇將rank低的根指向rank高的根可以降低樹高,提高查詢效率。程式碼如下:

class Union_Set {
private:
    int size;
    vector<int> next;
    vector<int> rank;
public:
    Union_Set(int size) {
        this->size = size;
        next.resize(size);
        rank.resize(size);
    }
    void makeSet() {
        for(int i = 0; i < size; i++) {
            next[i] = i;
            rank[i] = 1;
        }
    }
    int find(int i) {
        if(i != next[i])
            next[i] = find(next[i]);
        return next[i];
    }
    void Union(int i, int j) {
        int root_i = find(i);
        int root_j = find(j);
        if(root_i != root_j) {
            if(rank[root_i] < rank[root_j]) {
                next[root_i] = root_j;
                rank[root_j] += rank[root_i];
            }
            else {
                next[root_j] = root_i;
                rank[root_i] += rank[root_j];                
            }
        }
    }
};

 那麼如何使用並查集呢?我們將之前的演算法中的BFS改為並查集演算法即可,當二分法選取出一個t值時,我們遍歷一遍所有座標,如果這個座標的值不大於t,那麼將它與它上下左右的值不大於t的座標納入同一個集合中。處理完之後如果起點和終點在一個集合中,說明這個t值滿足條件。(需要注意每次處理之前都要重新呼叫makeSet,清理上次處理的結果)

class Solution {
public:
    int swimInWater(vector<vector<int>>& grid) {
        if(grid.size() == 0)
            return 0;
        int height = grid.size();
        int width = grid[0].size();
        int top = width*height-1;
        Union_Set s(height*width);
        int low = grid[0][0];
        int x_diff[4] = {0,0,1,-1};
        int y_diff[4] = {1,-1,0,0};
        while(1) {
            s.makeSet();
            int mid = (low+top)/2;
            for(int j = 0; j < height; j++)
            for(int k = 0; k < width; k++) {
                if(grid[j][k] > mid)
                    continue;
                for(int i = 0; i < 4; i++) {
                    if(j+x_diff[i] < 0 || j+x_diff[i] >= height ||
                        k+y_diff[i] <0 || k+y_diff[i] >= width)
                        continue;
                    if(grid[j+x_diff[i]][k+y_diff[i]] <= mid) {
                        s.Union(j*width+k,(j+x_diff[i])*width+(k+y_diff[i]));                      
                    }
                }             
            }
            if(s.find(0) == s.find(width*height-1)) {
                top = mid;
                if(top == low)
                    return top;
            } else {
                if(mid == low)
                    return top;
                low = mid;
            }
        }
    }
};

然後效率得到了提升,beat 30%+。那麼同樣使用並查集,還有更快的演算法嗎?參考下他人的演算法,這樣寫可以beat 98%,程式碼如下:

class Solution {
public:
    int swimInWater(vector<vector<int>>& grid) {
        if(grid.size() == 0)
            return 0;
        int size = grid.size();
        Union_Set s(size*size);
        s.makeSet();
        vector<pair<int,int>> v;
        v.resize(size*size);
        for(int i = 0; i < size; i++)
        for(int j = 0; j < size; j++) {
            v[grid[i][j]] = make_pair(i,j);
        }
        int x_diff[4] = {0,0,1,-1};
        int y_diff[4] = {1,-1,0,0};
        for(int t = 0; t < size*size; t++) {
            pair<int,int> tmp = v[t];
            for(int i = 0; i < 4; i++) {
                if(tmp.first+x_diff[i] < 0 || tmp.first+x_diff[i] >= size ||
                    tmp.second+y_diff[i] <0 || tmp.second+y_diff[i] >= size)
                    continue;
                if(grid[tmp.first+x_diff[i]][tmp.second+y_diff[i]] <= t) {
                    s.Union(tmp.first*size+tmp.second,(tmp.first+x_diff[i])*size+(tmp.second+y_diff[i]));                      
                }
            }             
            if(s.find(0) == s.find(size*size-1)) {
                return t;
            }
        }
    }
};

思路就是,因為二維矩陣是一個n x n的矩陣,且為0到(n x n) - 1 的置換。t從0開始逐1增加,每增一次就對那個剛好滿足的座標進行一次同樣的處理(上下左右是否滿足不大於t,滿足則Union)。在這一次遍歷的過程中,如果起點和終點在同一個集合中,那麼此時t就是滿足條件的最小t。

至於這個演算法的正確性證明,我也不太清楚,歡迎評論解答。

實際上不使用並查集也可以達到相同的執行時間。使用類似於Prim演算法求最小生成樹的演算法,S = {}, 初始時將原點新增到S中,然後每次從S中刪除值最小的點,記錄它的值到集合P,再將它鄰接的沒有訪問過的點加入S中。當刪除的點為終點時,集合P中最大的值即為最小的t值。程式碼如下,使用優先佇列以減少複雜度(此程式碼來自評論,侵刪):

 struct Pos {
     int x;
     int y;
     int elevation;
     Pos(int xx, int yy, int e) {
         x = xx;
         y = yy;
         elevation = e;
     }
 };

 struct PosComparer {
     bool operator()(const Pos &left, const Pos&right) {
         return left.elevation > right.elevation;
     }
 };

 class Solution {
 public:
     int swimInWater(vector<vector<int>>& grid) {
         int m = grid.size();
         if (m <= 0) {
             return 0;
         }
         int n = grid[0].size();
         if (n <= 0) {
             return 0;
         }
         if (m == 1 && n == 1) {
             return 0;
         }

         vector<vector<int>> directions{ { 0,1 },{ 0,-1 },{ 1,0 },{ -1,0 } };
         priority_queue<Pos, vector<Pos>, PosComparer> q;
         vector<vector<bool>> inQueue(m, vector<bool>(n));
         q.push(Pos(0, 0, grid[0][0]));
         inQueue[0][0] = true;
         int result = 0;
         while (q.size() > 0) {
             auto curPos = q.top();
             q.pop();

             result = max(result, curPos.elevation);
             if (curPos.x == m - 1 && curPos.y == n - 1) {
                 return result;
             }

             for (int i = 0; i < directions.size(); i++) {
                 int nx = curPos.x + directions[i][0];
                 int ny = curPos.y + directions[i][1];

                 if (nx >= 0 && nx < m && ny >= 0 && ny < n && !inQueue[nx][ny]) {
                     inQueue[nx][ny] = true;
                     q.push(Pos(nx, ny, grid[nx][ny]));
                 }
             }
         }

         return INT_MAX;
     }
 };