小米麵試題-朋友圈問題(並查集)
問題:
假設已知有n個人和m對好友關係(存於陣列r)。如果兩個人是直接或間接的好友(好友的好友的好友…),則認為他們屬於同一個朋友圈。
請寫程式求出這n個人裡一共有多少個朋友圈。
假如:n = 5,m = 3,r = {{1 , 2} , {2 , 3} , {4 , 5}},表示有5個人,1和2是好友,2和3是好友,4和5是好友,則1、2、3屬於一個朋友圈,4、5屬於另一個朋友圈,結果為2個朋友圈。
可以用並查集解決。
一、什麼是並查集
在一些應用問題中,需要將n個不同的元素劃分成一組不相交的集合。開始時,每個元素自成一個單元素集合,然後按一定的規律將歸於同一組元素的集合合併。在此過程中要反覆用到查詢某一個元素歸屬於那個集合的運算。
適合於描述這類問題的抽象資料型別稱為並查集。
實現並查集的一個典型方法是採用樹形結構來表示元素及其所屬子集的關係。
每個集合以一棵樹表示,樹的每一個結點代表集合的一個單元素。所有各個集合的全集合構成一個森林,並用樹與森林的父指標表示來實現。其下標代表元素名。第i個數組元素代表集合元素i 的樹結點。樹的根節點的下標代表集合名 稱,根節點的父為-1,表示集合中的元素個數。
二、舉例
假如全集合s 為:s = {0,1,2,3,4,5 ,6 , 7,8 ,9 },開始時,每個元素就是一個集合,如下:
第一步:初始化,我們用-1初始化每一個樹的根結點
第二步:將集合中的元素合併成三個子集合,它們是全集合s 的子集合:
s1 = {0,6, 7 , 8} ,s2 = {1,4, 9 } ,s3 = {2,3, 5 }。
其樹形結構如下:
按照上面樹的結構,假設我們的陣列名為arr,我們需要更新陣列的值:
(eg:a[0]=a[0]+a[6]+a[7]+a[8],所以為-4,a[6]=a[7]=a[8]=0(父節點))
以此類推,如下:
根據上述的演算法,陣列中值的負數的絕對值表示的是集合中元素的個數;負數的個數表示集合的個數;非負的數字表示的其下標對應的父結點。
第三步:求並集
比如題目中:
首先,我們應該判斷,4或9和另一個集合中任意節點對應的父結點是不是同一個結點。
如果是同一個,則說明他們本身就在同一個集合中;
如果不是同一個,則先找到兩個集合的父結點——0和1
再執行:
a[0]=a[0]-a[1]; a[1]=0(1的父結點);
此時陣列變為:
三、程式碼
並查集:
#include<iostream>
#include<vector>
using namespace std;
class UnionFind
{
public:
UnionFind(size_t size)
:_set(size,-1) //用-1初始化
{
//_set.resize(size, -1);
//_set.assign(size, -1);
}
void Union(int a,int b) //求交集
{
int root1 = FindRoot(a);
int root2 = FindRoot(b);
if (root1 != root2)
{
_set[root1] += _set[root2];
_set[root2] = root1;
}
}
//合併後集合的個數
size_t Count()
{
size_t count = 0;
for (size_t i = 0; i < _set.size(); ++i)
{
if (_set[i] < 0)
{
count++;
}
}
return count;
}
private:
int FindRoot(int x) //查詢父節點
{
while (_set[x] >= 0)
{
x = _set[x];
}
return x;
}
vector<int> _set;
};
接下來我們看上面的問題,可以這樣解決:
size_t Friends(const int m, const int n, int arr[][2])
{
//因為題目給出的編號,為符合實際,所以我們不使用0號位置
UnionFind uf(m + 1);
for (int i = 0; i < n ; i++)
uf.Union(arr[i][0], arr[i][1]);
return uf.Count() - 1;
}
void UFTest()
{
const int m = 5;
const int n = 3;
int r[3][2] = { { 1, 2 }, { 2, 3 }, { 4, 5 } };
cout << "朋友圈個數:" << Friends(m, n, r) << endl;
}