跳转至

基本思路O(n)

数据结构

class UF {
    // 记录连通分量
    int _count;
    // 节点 x 的父节点是 parent[x]
    vector<int> parent;

public:
    // 构造函数,n 为图的节点总数
    UF(int n) {
        // 一开始互不连通
        this->_count = n;
        // 父节点指针初始指向自己
        parent = vector<int>(n);
        for (int i = 0; i < n; i++)
            parent[i] = i;
    }

    // 其他函数
};

方法

class UF {
// 为了节约篇幅,省略上文给出的代码部分...

private:
    // 返回某个节点 x 的根节点
    int find(int x) {
        // 根节点的 parent[x] == x
        while (parent[x] != x)
            x = parent[x];
        return x;
    }

public:
    //合并树
    void union_(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        if (rootP == rootQ)
            return;
        // 将两棵树合并为一棵
        parent[rootP] = rootQ;
        // parent[rootQ] = rootP 也一样

        // 两个分量合二为一
        _count--;
    }
    // 返回当前的连通分量个数
    int count() {
        return _count;
    }


    //判断是否为连通分量
    bool connected(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        return rootP == rootQ;
    }
};

三、平衡性优化 O(1)

上面这种解法,find , union , connected 的时间复杂度都是 O(N)。这个复杂度很不理想的,诸如社交网络这样数据规模巨大的问题,对于 union 和 connected 的调用非常频繁,每次调用需要线性时间完全不可忍受。

我们其实是希望,小一些的树接到大一些的树下面,这样就能避免头重脚轻,更平衡一些。 结构(新增重量):

class UF {
private:
    int _count;
    vector<int> parent;
    // 新增一个数组记录树的“重量”
    vector<int> size;

public:
    UF(int n) {
        this->_count = n;
            this->parent.resize(n);
        // 最初每棵树只有一个节点
        // 重量应该初始化 1
        this->size.resize(n); 
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            size[i] = 1;
        }
    }
    // 其他函数
};

修改union方法:

class UF {
private:
    // 为了节约篇幅,省略上文给出的代码部分...

public:
    void union_(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        if (rootP == rootQ)
            return;

        // 小树接到大树下面,较平衡
        if (size[rootP] > size[rootQ]) {
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
        } else {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
        }
        _count--;
    }
};

四、路径压缩

这步优化虽然代码很简单,但原理非常巧妙。

其实我们并不在乎每棵树的结构长什么样,只在乎根节点

因为无论树长啥样,树上的每个节点的根节点都是相同的,所以能不能进一步压缩每棵树的高度,使树高始终保持为常数? 这样每个节点的父节点就是整棵树的根节点,find 就能以 O(1) 的时间找到某一节点的根节点,相应的,connected 和 union 复杂度都下降为 O(1)。

1.find加一行代码

class UF {
    // 为了节约篇幅,省略上文给出的代码部分...

private:
    int find(int x) {
        while (parent[x] != x) {
            // 这行代码进行路径压缩
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }
};

2.递归find(效率更高)

压缩更彻底,完全压平树枝

class UF {
    // 为了节约篇幅,省略上文给出的代码部分...

    // 第二种路径压缩的 find 方法
    public:
        int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }
};