1. 程式人生 > >[資料結構與演算法]通俗易懂入門並查集

[資料結構與演算法]通俗易懂入門並查集

並查集,顧名思義,具有將兩個或以上的集合合併和查詢的作用。所以討論這個資料結構即討論兩個函式,一個是查詢函式find(),另一個是合併函式join()。 為了便於理解,我們從題目入手:  hdoj暢通工程

用leetcode的題目做例子,簡單的說就是,假設1和2是朋友,2和3是朋友,4和5是朋友,那1,2,3可以組成一個朋友圈,4和5可以組成另一個朋友圈,所以一共有兩個朋友圈。題目給出N個人之間的關係,問到底有幾個朋友圈。

解決題目前,我們先來看個故事:寵物小精靈裡面有熔岩隊和海洋隊,兩隊互為彼此的勁敵,雙方隊員們要是遇上難免會幹上一架,試想這樣的一個場景,假設你是熔岩隊的一名隊員,一天你在路上遇到了一個人,但你不確定對方到底是自己隊的還是海洋隊。為了弄清對方的身份,你會問對方:你的隊長是誰。如果對方回答:我的隊長是赤炎鬆(熔岩隊的隊長)。那你就會意識到,哦,原來跟我是一樣的隊長,那大家都是熔岩隊的,於是兩人就手牽手和和睦睦走在一起了。。。但要是對方回答:我的隊長是水梧桐(海洋隊的隊長)。嗯?居然跟我不是一個隊長,那我不能慫,來幹吧。。

從上面我們引出一個數組 p[i] = j。i代表某個人,j代表i這個人的隊長,套進上面的場景就是p[我] = 赤炎鬆。所以想要知道某某人屬於哪一個隊的,就等於求p[某某人] 等於什麼,然後就可以知道所屬隊伍。但是有一個臨界點:p[我] = 赤炎鬆 這個很容易理解,即我的隊長是赤炎鬆,但是p[赤炎鬆]又等於什麼呢?難道赤炎鬆上面還有隊長嗎?當然是沒有了,赤炎鬆已經是頂端的了,於是對於頂端的情況,我們另p[赤炎鬆] = 赤炎鬆,即p[i] = i(i已經是頂端的情況),對應的,p[水梧桐] = 水梧桐。

綜上,我們不難寫出一個find函式,用來確定我對上的是誰。

int find(int x)  //查詢x最頂端的上司是誰
{
	int r = x;
	while (p[r] != r)
		r = p[r];
	return r;
}

可能有些小夥伴會疑問:為什麼會有個while迴圈,又回到剛才的場景,假如熔岩隊存在分級制度,即:隊長->組長->隊員。

而“我”是屬於隊員這個級別,又假如我不知道自己的隊長是誰,也即最頂級的那個人是誰,但是我肯定知道比自己高一級的組長是誰,於是我可以通過問組長:組長組長,我們的隊長是誰啊?組長就會跟我說他對上的那個人是誰,而那個人就是我們的隊長了,這就是間接知道自己的隊長是誰。現在討論的是三個等級的情況,可能還會存在n個等級的情況,但是方法類似,通過一步步向上問,直到問到不能再問,那個人就是隊長了。於是我們可以通過找到隊長是誰的方式來確定兩個人到底是不是一個隊伍的。

突然有一天,熔岩隊和海洋隊的隊長決定以後要和睦相處不打架了,於是可以令p[赤炎鬆] = 水梧桐,也就是另水梧桐成為赤炎鬆的隊長,如此一來兩隊的隊長都是水梧桐了,當然也可以令p[水梧桐] = 赤炎鬆。於是可以寫出一個合併的函式

void join(int x, int y)
{
	int fx = find(x), fy = find(y);
	if (fx != fy)  
		pro[fx] = fy;
}

到此,我們已經把兩個需要的函式都寫完了,但是對於find函式我們還可以進行改進,如果存在n級的情況,我們每一次查詢自己的隊長是誰的時候都要一步一步往上問,也就是這樣的鏈式:我->組長->隊長。 當n越大,那查詢的效率越低,於是我們把這個鏈式改成這樣:我->隊長<-組長,也就是我們每一個人只需要一步便可查詢到隊長是誰,也就是壓縮路徑。對find函式的修改如下

int find(int x)
{
	int r = x;
	while (p[r] != r)
		r = p[r];   //找到r是最頂端的那一個
	int i = x, j;
	while (i != r)      //通過迴圈將每一個人都與最頂端的那個人連線起來
	{
		j = p[i];
		p[i] = r;
		i = j;
	}
	return r;
}

回到朋友圈的問題,我們只需一開始令每個人的朋友=自己本身,然後根據題意,誰跟誰是朋友,就誰跟誰呼叫join()函式連線在一起,最後利用find函式檢視並統計每個人頂端的那個人有幾個不同的人,便是幾個朋友圈。可以利用set去重也可以打表去重。

附上AC程式碼


class Solution {
public:
    int find(vector<int>& v,int x)
    {
        int r = x;
        while(v[r] != r)
            r = v[r];
        int i = x,j;
        while(i != r)
        {
            j = v[i];
            v[i] = r;
            i = j;
        }
        return r;
    }
    void join(vector<int>& v,int x,int y)
    {
        int fx = find(v,x),fy = find(v,y);
        if(fx!=fy)
            v[fx] = fy;
    }
    int findCircleNum(vector<vector<int>>& M) {
        vector<int> v(M.size());
        for(int i = 0;i<v.size();i++)
            v[i] = i;  //一開始每個人都是自己朋友
        for(int i = 0;i<M.size();i++)
        {
            for(int j = 0;j<M[i].size();j++)
            {
                if(i!=j&&M[i][j] == 1)
                {
                    join(v,i,j);
                }
            }
        }
        set<int> s;//利用set去重
        for(int i = 0;i<v.size();i++)
            s.insert(find(v,i));
        return s.size();
    }
};

hdoj那道題也是類似的方法解決,最後要修的道路數=不同的城市圈-1。在這就不貼程式碼了,自己嘗試用並查集做一遍吧。