并查集與類型推導(dǎo)

某次參加筆試的最后一題大意如下:給定一組用戶[0..n],以及他們之間的好友關(guān)系,問這些好友構(gòu)成了多少個朋友圈?
例如有用戶[1..5],好友關(guān)系有(1,2),(3,4),(4,5),則共有兩個好友圈:[1,2]和[3,4,5]

并查集

題目的意思可以理解為給定一個無向圖,求其中連通子圖的個數(shù)。算法來自于《算法導(dǎo)論(第二版)》的第21章
對于一個并查集來說,我們通常希望其支持三種操作:
MakeSet(x):當(dāng)我們新加入一個元素,且與當(dāng)前任何節(jié)點有連接,因此它(目前為止)單獨成為一個集合。
Union(x,y):添加兩個元素之間的聯(lián)系,因此兩個元素屬于的集合需要合并成為一個集合。
FindSet(x):查詢一個元素到底屬于哪個集合。
顯然當(dāng)有n個元素時,Union操作至多有n-1次,MakeSet操作至多為n次。

代碼實現(xiàn)

可以用一個數(shù)組p來儲存所有元素對應(yīng)的集合。例如p[x]=1表示元素x屬于編號為1的集合。然而整個編號為1的集合可能又屬于另一個更大的集合,因此需要查詢p[1]來找到其父集合……以此類推,直到有p[a]=a為止。由此可見,這其實是一種基于樹的實現(xiàn)。

圖片源自《算法導(dǎo)論》

  • 兩種策略
  • 按秩合并(union by rank)
    rank可以看作是每個根節(jié)點的高度(并不嚴(yán)格是,其實是其高度的一個上界),合并兩棵樹樹時,將高度較小的樹的根指向高度較大的樹可以保證合并后的樹的高度上界不再增加。若高度相等,則合并后的樹高度加一。
  • 路徑壓縮(path compression)
    當(dāng)從某一結(jié)點以此查找到根節(jié)點時,可以將經(jīng)過路徑的所有節(jié)點的父節(jié)點都修改為根節(jié)點。但我們的方法中并不修改根節(jié)點的秩,因為修改一顆樹的高度需要遍歷整棵樹的所有節(jié)點才能確定,這顯然得不償失,這也就解釋了為什么秩并不是每棵樹的高度,而是它高度的一個上界。
圖片源自《算法導(dǎo)論》
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)

類型推導(dǎo)

在一個函數(shù)體中,各個參數(shù)的相互作用可以看作是一個無向有環(huán)圖(DAG)。依據(jù)函數(shù)內(nèi)的各個節(jié)點,可以生成一系列的類型約束等式。

圖片來自Essentials of Porgramming languages

圖中的代碼proc(f)proc(x)-((f 3),(f x))等價于如下scheme代碼:

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

或者如下C++代碼

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

約束生成(constraints generation)的規(guī)則如下:


內(nèi)容來自Programming Languages and Lambda Calculi

得到類型等式后,接下來是對等式進(jìn)行合一(unify)和替換(substitute)操作。
合并規(guī)則如下:


內(nèi)容來自Programming Languages and Lambda Calculi

關(guān)于多態(tài)的推導(dǎo):

內(nèi)容來自Programming Languages and Lambda Calculi

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


可以看到,每一個等式,都需要進(jìn)行unify操作,因為類型相等即代表它們屬于同一集合。但需要注意的是,這里的unify操作和之前提到的union并不完全相同,因為我們還需要處理函數(shù)類型。若有類似t1->t2=t3->t4的等式,則需要同時unify(t1,t3)unify(t2,t4)。另一方面,在合并類型時可能是有方向的,需要將泛化(generic)類型向具化(normalized)類型合并。例如有t1=t2,t2=int,則得到t1=int,t2=int,這也意味這上文提到的按秩合并的策略在此并不可用,但路徑壓縮依然可行。

更多閱讀

  • 《算法導(dǎo)論》中關(guān)于并查集算法時間復(fù)雜度的證明
  • Hindley-Milner類型系統(tǒng)
  • 邏輯式編程與合一算法
  • Recursive Type(本文中的類型推導(dǎo)僅對STLC(simply typed lambda calculus)成立,根據(jù)STLC的strong normalization特性,所有STLC能表達(dá)的程序中不會有遞歸且一定終止,如果突破這一限制呢?)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容