并查集与类型推导

某次参加笔试的最后一题大意如下:给定一组用户[0..n],以及他们之间的好友关系,问这些好友构成了多少个朋友圈?
例如有用户[1..5],好友关系有(1,2),(3,4),(4,5),则共有两个好友圈:[1,2]和[3,4,5]

并查集

题目的意思可以理解为给定一个无向图,求其中连通子图的个数。算法来自于《算法导论(第二版)》的第21章
对于一个并查集来说,我们通常希望其支持三种操作:
MakeSet(x):当我们新加入一个元素,且与当前任何节点有连接,因此它(目前为止)单独成为一个集合。
Union(x,y):添加两个元素之间的联系,因此两个元素属于的集合需要合并成为一个集合。
FindSet(x):查询一个元素到底属于哪个集合。
显然当有n个元素时,Union操作至多有n-1次,MakeSet操作至多为n次。

代码实现

可以用一个数组p来储存所有元素对应的集合。例如p[x]=1表示元素x属于编号为1的集合。然而整个编号为1的集合可能又属于另一个更大的集合,因此需要查询p[1]来找到其父集合……以此类推,直到有p[a]=a为止。由此可见,这其实是一种基于树的实现。

并查集与类型推导_第1张图片
图片源自《算法导论》

  • 两种策略
  • 按秩合并(union by rank)
    rank可以看作是每个根节点的高度(并不严格是,其实是其高度的一个上界),合并两棵树树时,将高度较小的树的根指向高度较大的树可以保证合并后的树的高度上界不再增加。若高度相等,则合并后的树高度加一。
  • 路径压缩(path compression)
    当从某一结点以此查找到根节点时,可以将经过路径的所有节点的父节点都修改为根节点。但我们的方法中并不修改根节点的秩,因为修改一颗树的高度需要遍历整棵树的所有节点才能确定,这显然得不偿失,这也就解释了为什么秩并不是每棵树的高度,而是它高度的一个上界。
并查集与类型推导_第2张图片
图片源自《算法导论》
int MakeSet(int x)
{
    p[x]=x;
    rank[x]=0;
}

void Union(int a,int b)
{
    int x=FindSet(a);
    int y=FindSet(b);
    if(rank[x]>rank[y])
    {
        p[y]=x;
    }
    else
    {
        p[x]=y;
    }
    if(rank[x]==rank[y])
    {
        rank[y]=rank[y]+1;
    }
}

int FindSet(int x)
{
    if(x!=p[x])
    {
        p[x]=FindSet(p[x]);
    }
    return p[x];
}

MakeSetUnionFindSet操作共m个,其中有n个MakeSet操作,单独使用按秩合并的策略运行时间为


单独使用路径压缩,若有n个 MakeSet操作和f个 FindSet操作,运行时间为![]( http://latex.codecogs.com/svg.latex?\Theta\left(n+f\cdot\left(1+\textup{log}_{2+f/n}n\right)\right)

类型推导

在一个函数体中,各个参数的相互作用可以看作是一个无向有环图(DAG)。依据函数内的各个节点,可以生成一系列的类型约束等式。

并查集与类型推导_第3张图片
图片来自Essentials of Porgramming languages

图中的代码 proc(f)proc(x)-((f 3),(f x))等价于如下scheme代码:

(lambda (f) (lambda (x) (- (f 3) (f x))))

或者如下C++代码

template
T3 func(Tf f, Tx x)
{
  return (f 3)-(f x);
}

约束生成(constraints generation)的规则如下:


并查集与类型推导_第4张图片
内容来自Programming Languages and Lambda Calculi

得到类型等式后,接下来是对等式进行合一(unify)和替换(substitute)操作。
合并规则如下:


并查集与类型推导_第5张图片
内容来自Programming Languages and Lambda Calculi

关于多态的推导:

并查集与类型推导_第6张图片
内容来自Programming Languages and Lambda Calculi

下面是Essentials of Programming Languages中的步骤演示

并查集与类型推导_第7张图片

可以看到,每一个等式,都需要进行 unify操作,因为类型相等即代表它们属于同一集合。但需要注意的是,这里的 unify操作和之前提到的 union并不完全相同,因为我们还需要处理函数类型。若有类似 t1->t2=t3->t4的等式,则需要同时 unify(t1,t3)unify(t2,t4)。另一方面,在合并类型时可能是有方向的,需要将泛化(generic)类型向具化(normalized)类型合并。例如有 t1=t2,t2=int,则得到 t1=int,t2=int,这也意味这上文提到的 按秩合并的策略在此并不可用,但 路径压缩依然可行。

更多阅读

  • 《算法导论》中关于并查集算法时间复杂度的证明
  • Hindley-Milner类型系统
  • 逻辑式编程与合一算法
  • Recursive Type(本文中的类型推导仅对STLC(simply typed lambda calculus)成立,根据STLC的strong normalization特性,所有STLC能表达的程序中不会有递归且一定终止,如果突破这一限制呢?)

你可能感兴趣的:(并查集与类型推导)