<Re:从零开始的算法总结>(2) --并查集

引入--等价类

  1. 等价关系

    设集合A内有n各元素, R是集合 A 上的一个二元关系,若R满足:

    自反性:∀ a ∈A, => (a, a) ∈ R

    对称性:(a, b) ∈R∧ a ≠ b => (b, a)∈R

    传递性:(a, b)∈R,(b, c)∈R =>(a, c)∈R

    则称R是定义在A上的一个等价关系。设R是一个等价关系,若(a, b) ∈ R,则称a等价于b,记作 a ~ b

  2. 等价类

    等价类是指相互等价的元素的最大集合,最大意味着不存在类以外的元素与它们等价。且由于每一个元素只能属于一个等价类,所以等价关系把集合A划分不相交的等价类

  3. 等价类问题

    • 离线等价类问题:已知n和R,确定所有的等价类

    • 在线等价类问题(又称为并查集问题):初始有n个元素,每个元素都属于一个独立的等价类,需要执行以下操作:

      1. combine(a,b )把包含a和b的等价类合并成一个等价类,其思路如下

        classA = find(a);
        classB = find(b);
        if(classA!=classB)
            unite(classA,classB);
        
      2. find(theElement),确定元素theElement在哪一个类,目的是确定给定的两个元素是否在同一个等价类

并查集

  1. 定义

    在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。

  2. 主要操作

    • 初始化
      把每个点所在集合初始化为其自身。
      通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为O(N)。
    • 查找
      查找元素所在的集合。
    • 合并
      将两个元素所在的集合合并为一个集合。
      通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。
  3. 数据结构

    并查集算法可以基于数组、链表和森林的方式实现,往往使用森林实现以期获得更好的效率。

在线等价类问题的应用实例

机器调度问题

  1. 题目信息:某工厂有一台机器能执行n个任务,任务i的开始时间为整数ri ,截止时间为di,在该机器商完成每个任务都需要一个单元的时间,一种可行的调度方案时为每个任务分配相应的时间段,使得分配给任务i的时间正好位于开始时间和截止时间之间。同一个时间段不允许分配给多个任务

  2. 设计方法

      1. 按开始时间的非递增次序对任务进行排序
      1. 按开始时间的非递增次序考察任务,对于每一个任务,确定一个空闲时间段,这个时间段在截止时间之前,但与截止时间最为接近,如果这个时间段位于任务开始时间之前则分配失败,否则就把这个时间段分配给该任务。
  3. 方法实现:

    排序问题不过多解释。2)可进行如下处理

    ​ 令d为所有任务最后的截止时间,将可用时间段表示为从从 i-1到 i,其中1<=i<=d,称为时间段1到时间段d,对于任意一个时间段a,用near(a)表示空闲时间段i,其中i是在小于等于a范围内最大的i,如果不存在,则定义near(a)=near(0)=0。两个时间段a和b同属于一个等价类,当且仅当near(a)=near(b)。

    ​ 在任务调度之前,所有时间段都有near(a)=a,即每个时间段哦都是一个独立的等价类,当按步骤2)把时间段a分配给某个任务时,对于所有near(b)=a的时间段b,near(b)的新的near值修改为near(a-1)。

    ​ 因此,当把时间段a分配给一个任务时,需要对当前包含时间段a和a-1的等价类进行合并。

    ​ 如果每个等价类e用nearest返回其成员的near值,则near值酱油nearest[find(a)]给出

解决方案

基于数组

使用一个数组equivClass且令equiClass[i]为包含元素i的等价类,在合并两个不同的类时,从两者中人去一个类,然后把该类的所有元素的值修改为另一个类元素的值。

/*
*基于数组实现并查集算法*/
int *equivClass,n;//等价类数组和元素个数
void initialize(int number_of_elements){
	n = number_of_elements;
    equivClass = new int[n+1];
    for (int e = 1; e <= n;e++)
        equivClass[e] = e;
}

void unite(int classA,int classB){
    //合并类classA和classB
    //假设class A!=classB
    for (int k = 1; k <= n;k++){
        if(equivClass[k]==classB)
            equivClass[k] = classA;
    }
}

int find(int theElement){
    //查找具有元素theElement的类
    return equivClass[theElement];
}

基于链表

  • 数据结构

    结构体equivNode,具有数据成员equivClass、size和next

    类型为equivNode的数组node[1:n]用于描述n个元素,每个元素都有一个对应的等价类链表

    node[e].equivClass既是find(e)的返回值,也是一个整形指针,指向等价类node[e].equivClass的链表首结点

    只有e是链表的首结点才定义node[e].size,这是node[e].size表示从node[e]开始的链表的节点个数

    node[e].next给出了包含结点e的链表的下一个节点,因为结点从1到n编号,所以可以用0表示空指针NULL

  • 代码实现

    typedef struct equivNode{
        int equivClass; //元素类标识符
        int size;       //元素个数
        int next;       //类中下一个元素的指针
    } equivNode;
    
    equivNode *node;     //节点数组
    int n;              //元素个数
    void initialize(int number_of_elements){
        n = number_of_elements;
        node = new equivNode[n + 1];
    
        for (int e = 1; e <= n;e++){
            node[e].equivClass = e;
            node[e].size = 1;
            node[e].next = 0;
        }
    }
    
    void unite(int classA,int classB){
        //合并classA和classB
        //classA和classB是链表首元素
        
        //使classA成为较小的类
        if(node[classA].size>node[classB].size)
            swap(classA,classB);
    
        //改变较小的类的equivClass值
        int k;
        for (k = classA; node[k].next != 0;k=node[k].next)
            node[k].equivClass = classB;
        //处理最后一个节点
        node[classB].equivClass = classB;
    
        //在链表classB的首元素后插入链表classA
        //修改新链表大小
        node[classB].size+=node[classA].size;
        node[k].next = node[classB].next;
        node[classB].next = classA;
    }
    
    int find(int theElement){
        //查找包含元素theElement的类
        return node[theElement].equivClass;
    }
    

    表示合并过程的效果图

    上图表示一个简单的合并过程

算法分析

1次初始化和f次查找的复杂度为O(n+f),对于u此合并,每一次合并的性能为O(较小类的大小),合并过程中,小类移动到打雷,一次合并复杂度为O(移动元素个数),一次合并后心累的大小至少是原来小磊大小的两倍,由于操作结束时没有哪个类的元素数会超过u+1(此为数学结论),所以没有哪个元素元素的移动个数超过u+1,另外最多有2u个元素发生移动(数学结论),因此总移动次数不会超过2ulog2(u+1),综上n次合并复杂度为O(nlogn)

算法优化

按照前述的思路,单次查找的复杂度为O(n),单次合并的平均复杂度为O(logn),但是最坏情况下,复杂度为O(n)。

我们不再通过等价类的方式区分每一项,修改类的方式也不再是修改类标志,而是合并引入一个“祖先”概念模拟并查集。

现在我们假定 f[i] 表示第 i 个人的祖先是谁。

现在我们有甲,乙,丙三个人(分别用 a, b, c 表示)

假设经过考察,乙是甲的祖先。则有 f[a]=b,后来甲又证明是丙的祖先那么有 f[c]=a

但是如果我们这样表示,丙不能直接知道乙,容易一家人不认一家人

所以,我们必须直接让丙的祖先变成最大的祖先。定义如下find

//优化find
int find(int x){
	while(x!=fa[x]) x=fa[x]=fa[fa[x]];
}

再附加额外设定

  • 一个人不能有两个祖先。
  • 当已经有祖先的人查到更久远的祖先时,祖先也将成为更高祖先的子孙。

例题--洛谷P3367

题目描述

如题,现在有一个并查集,你需要完成合并和查询操作。

输入格式

第一行包含两个整数 N,MN,M ,表示共有 NN 个元素和 MM 个操作。

接下来 MM 行,每行包含三个整数 Zi,Xi,Yi

当 Zi=1时,将 Xi 与 Yi所在的集合合并。

当 Zi=2时,输出 Xi 与 Yi是否在同一集合内,是的输出 Y ;否则输出 N

输出格式

对于每一个 Z_i=2Z**i=2 的操作,都有一行输出,每行包含一个大写字母,为 Y 或者 N

代码

//优化前
#include<iostream>
#include<cstdlib>
using namespace std;
int Array[10005];
void unite(int classa,int classb,int n);
void initialize(int n);
int main() {
	int m, n,x,y,z;
	cin >> n >> m;
	initialize(n);
	for (int i = 0; i < m; i++) {
		cin >>z >> x >> y;
		if (z == 1) {
			unite(x, y, n);
		}
		else if(z == 2){
			if (find(x) == find(y))
				cout << "Y" << endl;
			else
				cout << "N" << endl;
		}
	}
}
void initialize(int n) {
	for (int i = 1; i <= n; i++) {
		Array[i] = i;
	}
}

int find(int theElement){
    return Array[theElement];
}

void unite(int classA, int classB,int n) {
	int a = find(classA);
	int b = find(classB);
	for (int i = 1; i <= n; i++) {
		if (find(i) == b)
			Array[i] = a;
	}
}
//优化后
#include<iostream>
#include<cstdlib>
using namespace std;
int Array[10005];
int find(int theElement);
void initialize(int n);
int main() {
	int m, n,x,y,z;
	cin >> n >> m;
	initialize(n);
	for (int i = 0; i < m; i++) {
		cin >>z >> x >> y;
		int a = find(x);
		int b = find(y);
		if (z == 1) {
			Array[a]=b;
		}
		else if(z == 2){
			if (a == b)
				cout << "Y" << endl;
			else
				cout << "N" << endl;
		}
	}
}
void initialize(int n) {
	for (int i = 1; i <= n; i++) {
		Array[i] = i;
	}
}

int find(int theElement) {
	while (theElement != Array[theElement]) theElement = Array[theElement] = Array[Array[theElement]];
	return theElement;
}


posted @ 2021-03-10 23:17  Faura_Sol  阅读(401)  评论(0)    收藏  举报
Live2D