并查集之习题分析

一、并查集的概念

​ 并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。

​ 并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。

二、并查集的数据结构以及代码分析

(一)、并查集的数据结构

public class UnionFind {
    
    HashMap<Integer, Integer> father = new HashMap<>();

    public UnionFind(Integer n) {
        for (int i = 0; i < n; i++) {
            father.put(i, i);
        }
    }

    public Integer find(Integer x) {
        Integer parent = father.get(x);
        while (!parent.equals(father.get(parent))) {
            parent = father.get(parent);
        }
        Integer temp = -1;
        Integer fa = father.get(x);
        while (!fa.equals(father.get(fa))) {
            temp = father.get(fa);
            father.put(fa, parent);
            fa = temp;
        }
        return parent;
    }

    public void union(Integer x, Integer y) {
        Integer faX = father.get(x);
        Integer faY = father.get(y);
        if (!faX.equals(faY)) {
            father.put(faX, faY);
        }
    }
}

(二)、并查集数据结构方法分析

1、通过并查集的概念,我们可以得出我们共需要3种方法

​ (1)、为每一个元素“i”创建一个新的集合

​ (2)、返回元素“x”所在集合的根节点

​ (3)、合并元素“x”所在的集合”A“和元素“y”所在集合”B“两个集合

2、输入数值n,为n以内的每一个元素皆创建一个集合

public UnionFind(Integer n) {
    for (int i = 0; i < n; i++) {
        father.put(i, i);
    }
}

3、两个集合的合并,通过判断元素x与元素y所在的集合是否为同一集合,若不为同一集合,则将x所在的集合合并至y所在的集合内

public void union(Integer x, Integer y) {
    Integer faX = father.get(x);
    Integer faY = father.get(y);
    if (!faX.equals(faY)) {
        father.put(faX, faY);
    }
}

4、发现元素x所在的集合

​ (1)、首先不断判断x元素的父节点与其祖父节点(父节点的父节点)是不是同一节点,若为同一节点,则表示找到了最终的根节点,并将parent赋值为根节点。

​ (2)、将x元素节点以及其往上的所有父辈、祖父辈等节点都将其父节点赋值为根节点。这样以后查找x的父辈等节点的所属集合时,便只需查找一次map便可得到。

​ (3)、返回根节点

public Integer find(Integer x) {
    Integer parent = father.get(x);
    while (!parent.equals(father.get(parent))) {
        parent = father.get(parent);
    }
    Integer temp = -1;
    Integer fa = father.get(x);
    while (!fa.equals(father.get(fa))) {
        temp = father.get(fa);
        father.put(fa, parent);
        fa = temp;
    }
    return parent;
}

三、小岛问题

(一)、题目需求

假设你设计一个游戏,用一个 m 行 n 列的 2D 网格来存储你的游戏地图。

起始的时候,每个格子的地形都被默认标记为「水」。我们可以通过使用 addLand 进行操作,将位置 (row, col) 的「水」变成「陆地」。

你将会被给定一个列表,来记录所有需要被操作的位置,然后你需要返回计算出来 每次 addLand 操作后岛屿的数量。

注意:一个岛的定义是被「水」包围的「陆地」,通过水平方向或者垂直方向上相邻的陆地连接而成。你可以假设地图网格的四边均被无边无际的「水」所包围。

请仔细阅读下方示例与解析,更加深入了解岛屿的判定。

示例:

输入: m = 3, n = 3, positions = [[0,0], [0,1], [1,2], [2,1]]
输出: [1,1,2,3]
解析:

起初,二维网格 grid 被全部注入「水」。(0 代表「水」,1 代表「陆地」)

0 0 0
0 0 0
0 0 0
操作 1:addLand(0, 0) 将 grid[0][0] 的水变为陆地。

1 0 0
0 0 0 Number of islands = 1
0 0 0
操作 2:addLand(0, 1) 将 grid[0][1] 的水变为陆地。

1 1 0
0 0 0 岛屿的数量为 1
0 0 0
操作 3:addLand(1, 2) 将 grid[1][2] 的水变为陆地。

1 1 0
0 0 1 岛屿的数量为 2
0 0 0
操作 4:addLand(2, 1) 将 grid[2][1] 的水变为陆地。

1 1 0
0 0 1 岛屿的数量为 3
0 1 0

拓展:

你是否能在 O(k log mn) 的时间复杂度程度内完成每次的计算?(k 表示 positions 的长度)

(二)、解法

  • Point
public class Point {

    int x;
    int y;

    public Point() {
        this.x = 0;
        this.y = 0;
    }

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}
  • NumberOfIsland2
public class NumberOfIsland2 {

    class UnionFind {
        private HashMap<Integer, Integer> father = new HashMap<Integer, Integer>();

        public UnionFind(int row, int column) {
            for (int i = 0; i < row; i++) {
                for (int j = 0; j < column; j++) {
                    Integer id = convertId(i, j, row);
                    father.put(id, id);
                }
            }
        }

        public Integer convertId(int x, int y, int row) {
            return x * row + y;
        }

        public Integer find(int x) {
            Integer parent = father.get(x);
            while (!parent.equals(father.get(parent))) {
                parent = father.get(parent);
            }
            int temp = -1;
            Integer fa = father.get(x);
            while (!fa.equals(father.get(fa))) {
                temp = father.get(fa);
                father.put(fa, parent);
                fa = temp;
            }
            return parent;
        }

        public void union(int x, int y) {
            Integer faX = father.get(x);
            Integer faY = father.get(y);
            if (!faX.equals(faY)) {
                father.put(faX, faY);
            }
        }
    }

    public List<Integer> numIslands(int n, int m, Point[] operators) {
        List<Integer> result = new ArrayList<>();
        if (operators == null || operators.length == 0) {
            return result;
        }
        int count = 0;
        int[] dx = {0, 0, 1, -1};
        int[] dy = {1, -1, 0, 0};
        UnionFind unionFind = new UnionFind(n, m);
        boolean[][] isIsland = new boolean[n][m];

        for (Point point : operators) {
            int x = point.x;
            int y = point.y;
            if (!isIsland[x][y]) {
                isIsland[x][y] = true;
                count++;
                Integer id = unionFind.convertId(x, y, n);
                for (int k = 0; k < 4; k++) {
                    int newX = x + dx[k];
                    int newY = y + dy[k];
                    if (newX >= 0 && newX < n && newY >= 0 && newY < m && isIsland[newX][newY]) {
                        Integer newId = unionFind.convertId(newX, newY, n);
                        Integer father = unionFind.find(id);
                        Integer nfather = unionFind.find(newId);
                        if (!father.equals(nfather)) {
                            count--;
                            unionFind.union(nfather, father);
                        }
                    }
                }
            }
            result.add(count);
        }
        return result;
    }
}

(三)、代码分析

1、并查集的初始化与上面提到的示例有所不同,由于本题中,属于二维坐标,因此单纯以x,或y作为一个位置元素的标识则不可取,于是采取id = x * row + y的形式,为每一个位置元素赋值。

public UnionFind(int row, int column) {
    for (int i = 0; i < row; i++) {
        for (int j = 0; j < column; j++) {
            Integer id = convertId(i, j, row);
            father.put(id, id);
        }
    }
}

public Integer convertId(int x, int y, int row) {
    return x * row + y;
}

2、定义并初始化上下左右四个方向位置数组。同时使用并查集将每个位置元素创建一个集合。

int count = 0;
int[] dx = {0, 0, 1, -1};
int[] dy = {1, -1, 0, 0};
UnionFind unionFind = new UnionFind(n, m);
boolean[][] isIsland = new boolean[n][m];

3、循环遍历判断每个元素的情况

(1)、判断每个位置元素是否为陆地元素,若为陆地元素,则将陆地数量+1。

(2)、判断该陆地元素上下左右四个方向是否存在其他陆地元素,若存在,则将陆地数量-1。并将其合并至同一集合中。

for (Point point : operators) {
    int x = point.x;
    int y = point.y;
    if (!isIsland[x][y]) {
        isIsland[x][y] = true;
        count++;
        Integer id = unionFind.convertId(x, y, n);
        for (int k = 0; k < 4; k++) {
            int newX = x + dx[k];
            int newY = y + dy[k];
            if (newX >= 0 && newX < n && newY >= 0 && newY < m && isIsland[newX][newY]) {
                Integer newId = unionFind.convertId(newX, newY, n);
                Integer father = unionFind.find(id);
                Integer nfather = unionFind.find(newId);
                if (!father.equals(nfather)) {
                    count--;
                    unionFind.union(nfather, father);
                }
            }
        }
    }
    result.add(count);
}
return result;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值