暴力法求解“微信群覆蓋”?
題目:求微信群覆蓋
微信有很多群,現進行如下抽象:
- 每個微信群由一個***的gid標識;
- 微信群內每個用戶由一個***的uid標識;
- 一個用戶可以加入多個群;
- 群可以抽象成一個由不重復uid組成的集合,例如:
- g1{u1, u2, u3}
- g2{u1, u4, u5}
可以看到,用戶u1加入了g1與g2兩個群。
畫外音,注意:
- gid和uid都是uint64;
- 集合內沒有重復元素;
假設微信有M個群(M為億級別),每個群內平均有N個用戶(N為十級別).
現在要進行如下操作:
(1) 如果兩個微信群中有相同的用戶,則將兩個微信群合并,并生成一個新微信群;
例如,上面的g1和g2就會合并成新的群:
- g3{u1, u2, u3, u4, u5};
畫外音:集合g1中包含u1,集合g2中包含u1,合并后的微信群g3也只包含一個u1。
(2) 不斷的進行上述操作,直到剩下所有的微信群都不含相同的用戶為止;
將上述操作稱:求群的覆蓋。
設計算法,求群的覆蓋,并說明算法時間與空間復雜度。
畫外音:58同城2013年校招筆試題。
對于一個復雜的問題,思路肯定是“先解決,再優化”,大部分人不是神,很難一步到位。先用一種比較“笨”的方法解決,再看“笨方法”有什么痛點,優化各個痛點,不斷升級方案。
拿到這個問題,很容易想到的思路是:
- 先初始化M個集合,用集合來表示微信群gid與用戶uid的關系;
- 找到哪兩個(哪些)集合需要合并;
- 接著,進行集合的合并;
- 迭代步驟二和步驟三,直至所有集合都沒有相同元素,算法結束;
***步,如何初始化集合?
set這種數據結構,大家用得很多,來表示集合:
- 新建M個set來表示M個微信群gid
- 每個set插入N個元素來表示微信群中的用戶uid
set有兩種最常見的實現方式,一種是樹型set,一種是哈希型set。
假設有集合:
- s={7, 2, 0, 14, 4, 12}
樹型set的實現如下:
其特點是:
- 插入和查找的平均時間復雜度是O(lg(n))
- 能實現有序查找
- 省空間
哈希型set實現如下:
其特點是:
- 插入和查找的平均時間復雜度是O(1)
- 不能實現有序查找
畫外音:求群覆蓋,哈希型實現的初始化更快,復雜度是O(M*N)。
第二步,如何判斷兩個(多個)集合要不要合并?
集合對set(i)和set(j),判斷里面有沒有重復元素,如果有,就需要合并,判重的偽代碼是:
- // 對set(i)和set(j)進行元素判斷并合并
- (1) foreach (element in set(i))
- (2) if (element in set(j))
- merge(set(i), set(j));
***行(1)遍歷***個集合set(i)中的所有元素element;
畫外音:這一步的時間復雜度是O(N)。
第二行(2)判斷element是否在第二個集合set(j)中;
畫外音:如果使用哈希型set,第二行(2)的平均時間復雜度是O(1)。
這一步的時間復雜度至少是O(N)*O(1)=O(N)。
第三步,如何合并集合?
集合對set(i)和set(j)如果需要合并,只要把一個集合中的元素插入到另一個集合中即可:
- // 對set(i)和set(j)進行集合合并
- merge(set(i), set(j)){
- (1) foreach (element in set(i))
- (2) set(j).insert(element);
- }
***行(1)遍歷***個集合set(i)中的所有元素element;
畫外音:這一步的時間復雜度是O(N)。
第二行(2)把element插入到集合set(j)中;
畫外音:如果使用哈希型set,第二行(2)的平均時間復雜度是O(1)。
這一步的時間復雜度至少是O(N)*O(1)=O(N)。
第四步:迭代第二步與第三步,直至結束
對于M個集合,暴力針對所有集合對,進行重復元素判斷并合并,用兩個for循環可以暴力解決:
- (1)for(i = 1 to M)
- (2) for(j= i+1 to M)
- //對set(i)和set(j)進行元素判斷并合并
- foreach (element in set(i))
- if (element in set(j))
- merge(set(i), set(j));
遞歸調用,兩個for循環,復雜度是O(M*M)。
綜上,如果這么解決群覆蓋的問題,時間復雜度至少是:
- O(M*N) // 集合初始化的過程
- +
- O(M*M) // 兩重for循環遞歸
- *
- O(N) // 判重
- *
- O(N) // 合并
畫外音:實際復雜度要高于這個,隨著集合的合并,集合元素會越來越多,判重和合并的成本會越來越高。
基于“先解決,再優化”的思想,很多優化方向的問題,自然而然的從腦中蹦出:
(1) 能不能快速通過元素定位集合?
畫外音:
- 通過集合查元素,哈希型set時間復雜度是O(1);
- 通過元素查集合(句柄),如何來實現呢?
(2) 能不能快速進行集合合并?
(3) 能不能一次合并多個集合?
經典數據結構,分離集合(disjoint set),它有三類操作:
- Make-set(a):生成一個只有一個元素a的集合;
- Union(X, Y):合并兩個集合X和Y;
- Find-set(a):查找元素a所在集合,即通過元素找集合;
特別適合用來解決這類集合合并與查找的問題,又稱為并查集。
如何利用并查集來解決求“微信群覆蓋”問題,是后文將要介紹的內容。
畫外音:先介紹“并查集”這一種方案,后續再介紹其他方案。
知道并查集的思路和原理,比知道什么是并查集更重要。
算法,其實還是挺有意思的。
【本文為51CTO專欄作者“58沈劍”原創稿件,轉載請聯系原作者】