并查集算法题

简要介绍:

并查集:

  一种数据结构,将一个集合分为不相交的子集,在添加新的子集,合并现有子集,判断元素所在集合时时间复杂度接近常数级。常用于求连通子图和最小生成树的Kruskal算法。

操作:

  makeSet:  初始化,给每个元素分配一个特定的id,以及一个指向自己的指针,表示每个元素都在一个大小为1的集合当中。

  find:       查找某个元素的根元素。当两个元素拥有同样的根元素时,说明他们在同一个集合当中。为了使得时间复杂度接近常数级,在查找的过程中,可以更新指针指向根元素(代码中使用递归方法),有人称其为路径压缩。

  Union:      满足条件则合并两个子集。如果没有特殊要求,将一个集合的根指向另一个集合的根即可。也可根据秩或者集合的大小来指定。

leetcode 128

并查集算法题_第1张图片

 思路: 使用并查集,比较两个数,如果他们的差为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& nums) {
        map m;
        int size = nums.size();
        for(int i = 0; i < size; i++)
            m.insert(pair(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

并查集算法题_第2张图片

思路:

首先想到是用BFS,对于给定的t执行BFS,如果能访问到终点,说明t满足条件。为了减少BFS的次数,可以采用二分的办法,选取指定的t。代码如下:

class Solution {
public:
    int swimInWater(vector>& 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 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> visited = {make_pair(0,0)};
            queue> q;
            q.push(make_pair(0,0));
            int mid = (low+top)/2;
            int s = q.size();
            while(!q.empty()){
                pair 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 n = pair(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 next;
    vector 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>& 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>& grid) {
        if(grid.size() == 0)
            return 0;
        int size = grid.size();
        Union_Set s(size*size);
        s.makeSet();
        vector> 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 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>& 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> directions{ { 0,1 },{ 0,-1 },{ 1,0 },{ -1,0 } };
         priority_queue, PosComparer> q;
         vector> inQueue(m, vector(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;
     }
 };

 

你可能感兴趣的:(leetcode)