拓扑排序(Topological Sorting)

 

并查集(Union Find Set)

这篇文章我们来讲一下并查集,什么是并查集呢?来举一个形象的例子。
并查集(Union Find Set)_并查集

话说符文之地上居住着各式各样的英雄,他们效忠于自己的国家或部落,整天背着武器在外面执行任务,碰到和自己不是一队的,就免不了要打一架。英雄们都有一个优点就是讲义气,绝对不打自己的队友,而且他们信奉“队友的队友就是我的队友”,只要是能通过队友关系串联起来的,不管拐了多少个弯,都认为是自己人。这样一来,不在同一个阵营的人,无论如何都无法通过队友关系连起来,就可以放心往死里打了。
两个原本互不相识的人,怎么判断是否属于一个阵营呢?我们可以在每个阵营内推举出一个比较有名望的人,作为该阵营的代表人物。比如德玛西亚的君主虽然是选举出来的,但每一代的嘉文都能成功登上王位。现任君主是嘉文三世,他的儿子基本可以确定是下一代君主。这样,每个阵营都有了自己的领军人物,或者叫队长,两人开打之前,只要互相确认一下自己的队长是不是同一个人,就可以确定敌友关系了。
并查集(Union Find Set)_并查集_02

还有一个问题啊,英雄们只知道自己直接的队友是谁,很多人压根就不认识队长,要判断自己的队长是谁,只能漫无目的的通过队友的队友关系问下去:“你是不是队长?你是不是队长?”这样一来,队长面子上挂不住了,而且效率太低,还有可能陷入无限循环中。于是队长下令,重新组队,队内所有人实行分等级制度,形成树状结构,队长就是根节点,下面分别是二级队员、三级队员,每个人只要记住自己的上级是谁就行了。遇到判断敌友的时候,只要一层层向上问,直到最高层,就可以在短时间内确定队长是谁了。

int setSize = 0;
unordered_map<int, int> father;
int find(int x) {
    int root = x;
    while (this->father[root] != -1) {
        root = this->father[root];
    }
    return root;
}

并查集(Union Find Set)_并查集_03

有一天,远在地球的孙悟空,发现了能量波动的符文大陆,于是分身来到了这里,这对于符文大陆来说,孙悟空属于新来的英雄,不属于任何阵营,那么它的上级就是它自己。我们定义一个add函数,参数x表示新英雄的ID,首先得确保这个ID不在原来的任何一个阵营里,然后把它的上级设置成-1表示它就是队长,最后让阵营的数量+1。

void add(int x) {
    if (!this->father.count(x)) {
        this->father[x] = -1;
        this->setSize++;
    }
}

并查集(Union Find Set)_算法模板_04

孙悟空来到符文大陆之后,由于旅途的奔波,让大圣不负原来世界的威能,经过猴子长者的指导,去北方寻找猴群,偶遇易大师,拜于无极剑道。易大师后来带猴子去阿狸的包子铺吃包子,阿狸见猴子无家可归,很可怜,就收留了猴子,这两个动物日久生情,成为了跨越种族的情侣。
九尾妖狐阿狸是属于艾欧尼亚阵营的,猴子也就顺理成章的加入了艾欧尼亚(找了个有户口的女人),怎么加入呢?

bool merge(int x, int y) {
    int rootX = this->find(x);
    int rootY = this->find(y);
    if (rootX != rootY) {
        father[rootX] = rootY;
        this->setSize--;
    }
}

我们把猴子的ID-x和阿狸的ID-y都传进merge函数中,然后通过find函数分别找到x和y的根节点,如果rootX != rootY,说明两个节点不是属于一个阵营的,我们把father[rootX] = rootY,这样就相当于把x加入到了y的阵营,最后让阵营的数量-1。
并查集(Union Find Set)_并查集_05

有一天,猴子出去执行任务,正好遇到了岩雀塔莉垭,但他俩互相不认识,不知道能不能动手,就开始找自己的上头问,猴子就去问自己的师傅无极剑圣——易大师,认不认识一个在石头上滑滑板的小孩,岩雀也去问自己的师傅疾风剑豪——亚索,认不认识一只拿着棒子的猴子。剑圣表示不认识啊,我去问问艾欧尼亚的领袖卡尔玛,亚索也表示,不知道什么猴子,也得去问问卡尔玛。一路问上去,最后才确定,两个人都是艾欧尼亚的。

并查集(Union Find Set)_二级_06

这样一连串的问太麻烦了,怎么办呢,干脆把所有的队员都直接挂在队长的旗下,出了事直接问队长,这样效率也高,所以我们得把整个阵营的层数维持在较低的水平上,也就是路径压缩算法。

int find(int x) {
    int root = x;
    while (this->father[root] != -1) {
        root = this->father[root];
    }
    while (x != root) {
        int original = this->father[x];
        this->father[x] = root;
        x = original;
    }
    return root;
}
算法模板
class UnionFindSet{
	private:
		int setSize = 0;
		unordered_map<int, int> father;
	public:
		int find(int x) {
			int root = x;
			while (this->father[root] != -1) {
				root = this->father[root];
			}
			while (x != root) {
				int original = this->father[x];
				this->father[x] = root;
				x = original;
			}
			return root;
		}
		bool isUnion(int x, int y) {
			return this->find(x) == this->find(y);
		}
		void merge(int x, int y) {
			int rootX = this->find(x);
			int rootY = this->find(y);
			if (rootX != rootY) {
				father[rootX] = rootY;
				this->setSize--;
			}
		}
		void add(int x) {
			if (!this->father.count(x)) {
				this->father[x] = -1;
				this->setSize++;
			}
		}
}; 
应用
  1. 检查 graph 中是否存在环