# Fiduccia Mattheyses :::warning 進入本篇內容之前,需要先了解 HyperGraph 的基本概念及結構,並且在此僅涉及實作,不包含演算法原理、算式解析。 ::: --- ## 符號/單辭定義 | 符號 | 定義 | | --------- | --------------------------------------------------- | | **S** | 所有頂點的集合 | | **Lₛ** | 經演算法分割後的子集合1 | | **Rₛ** | 經演算法分割後的子集合2 | | gain | 移動該頂點造成的收益(可能為負) | | cut edge | 若一超邊為 cut edge,則他同時包含 Lₛ 和 Rₛ 中的頂點 | | lhs / rhs | 左側 / 右側 | --- ## 概述 FM演算法本質目的為將一集合 **S** 分割為兩個子集合 **Lₛ**, **Rₛ**,且具以下規範 1. **Lₛ** 和 **Rₛ** 之間的關聯越**低**越好 (即為 cut edge 越少越好) 2. 在子集合中,頂點之間的關聯越**高**越好 3. 能夠在線性時間下完成 --- ## 資料結構 在實作演算法之前,首先定義 HyperGraph 的結構 ### HyperGraph | 資料 | usage | | ------------------ | ---------------------- | | VerticesMap | 即為所有頂點集合 **S** | | EdgesMap | 所有超邊的集合 | ### Vertex | 資料 | usage | | -------------- | ------------------------------------- | | ID | 在同一頂點集合內將會是唯一的 | | Name | Debug | | ConnectedEdges | 包含該頂點的超邊集合 | | Neighbors | 能通過 **1** 條超邊抵達的其他頂點集合 | ### Edge | 資料 | usage | | ----------------- | ---------------------------- | | ID | 在同一超邊集合內將會是唯一的 | | Name | Debug | | ConnectedVertices | 該超邊包含的所有頂點 | --- ## 演算法實作 ### gain 值計算公式 1. 當移動一個頂點會產生新的 Cut Edge 時,gain\-\- 2. 當移動一個頂點會減少一個 Cut Edge 時,gain++ 3. gain += (同子集鄰居數量 - 不同子集鄰居數量) * (映射至 +-1) ### 1. 初始化 Trait 對於在 Graph 中的所有頂點建立其特徵,包含以下資訊 在初始化過程中, IsLHS 將依照奇偶數去賦值 (任意賦值) | name | usage | | ------ | ---------------------------------------------------------------------- | | Sorted | 是否被排序過 | | IsLHS | 該點的所在的子集合<br>&emsp;T: 在 **Lₛ** 中 <br> &emsp;F: 在 **Rₛ** 中 | | Gain | 該頂點的 gain 值 | ### 2. 迭代演算 #### 2.1 找出 **Lₛ** 及 **Rₛ** 中 Gain 值最大的頂點 被標記為 Sorted 的頂點將不再列入計算,當沒有符合的頂點時,結束演算法。 後續把找到的兩個結果稱為 lmax 及 rmax #### 2.2 判斷要移動的節點 - 在左右子集中的頂點數量失衡時,優先嘗試回復平衡 - 當 lmax 及 rmax 的 gain 相等時,可以隨機選用, 在此實作中選擇能夠讓子集合連結性更高的頂點。 - 若 gain 不相等時,選擇 gain 值較高者 #### 2.3 移動該頂點並標記為 Sorted #### 2.4 更新可能受影響的 gain 值 gain 值只有在鄰居變動時才會更動,因此只須更新移動的頂點的鄰居頂點即可 ### 3. 輸出結果 根據最終特徵將 **S** 拆分為 **Lₛ**, **Rₛ** 並計算 Cut Edge 的數量 ### 補充 - gain 值計算中的第3點並非原始演算法中的內容,其目的是添加頂點間的群聚性,讓原始 gain 值相同時優先把小集群合併至大集合 - 在 2.2 中的頂點數量失衡部分,在此實作中使用一個 [0, 0.5] 浮點數 `split` 來表達一個子集合最少必須擁有多少比例的頂點 --- ## 演算效能 :::info 該實作未經嚴謹的效能優化,此表僅供參考 ::: - 連接數為一個超邊可包含的最小、最大頂點數 ![](https://hackmd.io/_uploads/Hk8QWX3ya.png) 通過簡易測試我們大致可以發現以下規律 - 整體演算法時間複雜度大約可以表示為 O(n\*m),其中 n 為頂點數,m 為超邊數,為線性關係。 - 失衡的超邊數量可能導致演算的時間大幅增加,如 1000 頂點,卻有 8000 個超邊。 - 在連結性更高的集合中 gain 值計算的成本會大幅增加,如超邊過多、超邊包含的頂點過多。 --- ## 演算結果 在此以一個簡易的超圖作為範例, 包含 8 的頂點 A~H ### case 1 超邊定義: 0.(A, B, C, D) 1.(E, F, G, H) ![](https://hackmd.io/_uploads/SyHHNm3JT.png)![](https://hackmd.io/_uploads/HyO9EX3yp.png) ### case 2 超邊定義: 0.(A, B, C, D) 1.(E, F, G, H) 2.(A, E) ![](https://hackmd.io/_uploads/r1OpE7nkp.png)![](https://hackmd.io/_uploads/BJ2K4mnkp.png) ### case 3 超邊定義: 0.(A, B, C, D) 1.(E, F, G, H) 2.(A, E) 3.(A, B, E) ![](https://hackmd.io/_uploads/HkDarX21T.png)![](https://hackmd.io/_uploads/rJmxHm316.png) --- ## 參考資料 - [Fiduccia-Mattheyses algorithm概述](https://blog.csdn.net/qinchun_bei/article/details/101067957) - [Fiduccia Mattheyses演算法(即FM演算法)的論文解讀](https://www.gushiciku.cn/pl/a8n0/zh-tw) - [超图学习(Learning with hypergraphs)(一)](https://zhuanlan.zhihu.com/p/361471954) - NCU Course (private) --- ## TODO - 效能優化 - 實作 Bucket - Trait 存儲模式 - Graph 相關物件建構 --- ## 原始碼 :::info 使用 VS2022 C++11 編寫 ::: ``` cpp= #include <iostream> #include <set> #include <map> #include <vector> #include <string> #include <stdlib.h> #include <algorithm> using namespace std; #pragma region class def class HyperGraph // 超圖 { public: class Vertex; class Edge; HyperGraph(); ~HyperGraph(); Vertex* GetVertex(int id, string name = ""); // 取得或創建新的 Vertex Edge* GetEdge(int id, string name = ""); // 取得或創建新的 Edge Vertex* CreateVertex(int id, string name = ""); // 創建新的 Vertex Edge* CreateEdge(int id, initializer_list<int> connectVertices = {}, string name = ""); // 創建新的 Edge,可初始化連接的 Vertices const map<int, Vertex*> VerticesMap(); // 取得超圖內所有頂點 const map<int, Edge*> EdgesMap(); // 取得超圖內所有超邊 friend std::ostream& operator<< (std::ostream& os, HyperGraph& obj); private: map<int, Vertex*> verticesMap; map<int, Edge*> edgesMap; }; class HyperGraph::Vertex { friend HyperGraph; friend Edge; public: ~Vertex(); const int ID; string Name = ""; const set<Edge*>& ConnectedEdges(); // 取得所有與該頂點相連的超邊 bool Contains(Edge* edge); // 該頂點是否連接輸入超邊 void Connect(Edge* edge); // 將該頂點連接至輸入超邊 void DisConnect(Edge* edge); // 將該頂點從輸入超邊斷開 set<Vertex*> GetNeighbors(); // 取得該頂點的鄰居 (能通過任意超邊抵達的另一頂點) friend std::ostream& operator<< (std::ostream& os, Vertex& obj); private: Vertex(); Vertex(int id, string name = ""); set<Edge*> connectedEdges; }; class HyperGraph::Edge { friend HyperGraph; friend Vertex; public: ~Edge(); const int ID; string Name = ""; const set<Vertex*>& ConnectedVertices(); // 取得所有與該超邊相連的頂點 bool Contains(Vertex* vertex); // 該超邊是否包含輸入頂點 void Connect(Vertex* vertex); // 將輸入超邊連接至該頂點 void DisConnect(Vertex* vertex); // 將輸入頂點從該超邊斷開 friend std::ostream& operator<< (std::ostream& os, Edge& obj); private: Edge(); Edge(int id, string name = ""); set<Vertex*> connectedVertices; }; class FiducciaMattheyses { public: struct Trait { bool Sorted = false; bool IsLHS; float Gain; }; FiducciaMattheyses(HyperGraph& g, float split = 0.5); // 建構子 & 實做函式 ~FiducciaMattheyses(); HyperGraph& Source; set<HyperGraph::Vertex*> LHS, RHS; set<HyperGraph::Edge*> CutEdges; friend std::ostream& operator<< (std::ostream& os, FiducciaMattheyses& obj); private: map<HyperGraph::Vertex*, Trait> traitTable; float con3step; // (1.0 / vertices count) void calGain(HyperGraph::Vertex* vertex); }; #pragma endregion //#define DEBUG int main(int argc, char* argv[]) { HyperGraph graph; {// Create Vertices int i = 0; for (auto str : { "A","B","C","D","E","F","G","H" }) graph.CreateVertex(i++, str); } // 實作演算法時預切分是奇偶分類,所以以下為預設最耦合的超邊連接 graph.CreateEdge(0, { 0,1,2,3 }); graph.CreateEdge(1, { 4,5,6,7 }); //graph.CreateEdge(2, { 0,4 }); //graph.CreateEdge(3, { 0,1,4 }); cout << graph << '\n'; auto fm = FiducciaMattheyses(graph, 0); cout << fm << '\n'; return 0; } #pragma region HyperGraph HyperGraph::HyperGraph() { } HyperGraph::~HyperGraph() { for (auto v : verticesMap) delete v.second; for (auto e : edgesMap) delete e.second; } HyperGraph::Vertex* HyperGraph::GetVertex(int id, string name) { if (!verticesMap.count(id)) verticesMap[id] = new Vertex(id, name); return verticesMap[id]; } HyperGraph::Vertex* HyperGraph::CreateVertex(int id, string name) { if (verticesMap.count(id)) { cout << "Already Contains Vertex " << id << '\n'; return nullptr; } auto v = GetVertex(id, name); return v; } const map<int, HyperGraph::Vertex*> HyperGraph::VerticesMap() { return this->verticesMap; } const map<int, HyperGraph::Edge*> HyperGraph::EdgesMap() { return this->edgesMap; } HyperGraph::Edge* HyperGraph::GetEdge(int id, string name) { if (!edgesMap.count(id)) edgesMap[id] = new Edge(id, name); return edgesMap[id]; } HyperGraph::Edge* HyperGraph::CreateEdge(int id, initializer_list<int> connectVertices, string name) { if (edgesMap.count(id)) { cout << "Already Contains Edge " << id << '\n'; return nullptr; } auto e = GetEdge(id, name); for (int v : connectVertices) e->Connect(GetVertex(v)); return e; } #pragma endregion #pragma region Vertex HyperGraph::Vertex::Vertex() :ID(-1) {} HyperGraph::Vertex::Vertex(int id, string name) :ID(id), Name(name) {} HyperGraph::Vertex::~Vertex() { } const set<HyperGraph::Edge*>& HyperGraph::Vertex::ConnectedEdges() { return this->connectedEdges; } bool HyperGraph::Vertex::Contains(Edge* edge) { return connectedEdges.count(edge); } void HyperGraph::Vertex::Connect(Edge* edge) { edge->Connect(this); } void HyperGraph::Vertex::DisConnect(Edge* edge) { edge->DisConnect(this); } set<HyperGraph::Vertex*> HyperGraph::Vertex::GetNeighbors() { auto result = set<Vertex*>(); for (auto e : ConnectedEdges()) { for (auto v : e->ConnectedVertices()) { if (v->ID == ID) continue; result.insert(v); } } return result; } #pragma endregion #pragma region Edge HyperGraph::Edge::Edge() :ID(-1) {} HyperGraph::Edge::Edge(int id, string name) :ID(id), Name(name) {} HyperGraph::Edge::~Edge() { } const set<HyperGraph::Vertex*>& HyperGraph::Edge::ConnectedVertices() { return this->connectedVertices; } bool HyperGraph::Edge::Contains(Vertex* vertex) { return connectedVertices.count(vertex); } void HyperGraph::Edge::Connect(Vertex* vertex) { if (Contains(vertex)) return; connectedVertices.insert(vertex); vertex->connectedEdges.insert(this); } void HyperGraph::Edge::DisConnect(Vertex* vertex) { if (!Contains(vertex)) return; connectedVertices.erase(vertex); vertex->connectedEdges.erase(this); } #pragma endregion #pragma region FiducciaMattheyses FiducciaMattheyses::FiducciaMattheyses(HyperGraph& g, float split) : Source(g), con3step(1.0 / Source.VerticesMap().size()) { // 預建置 Vertex Trait bool isOdd = true; for (auto pair : g.VerticesMap()) { traitTable[pair.second] = Trait(); traitTable[pair.second].IsLHS = isOdd; isOdd = !isOdd; } // 初始 Gain 值計算 for (auto pair : g.VerticesMap()) calGain(pair.second); int minSize = Source.VerticesMap().size(); int maxSize = minSize; minSize *= split; maxSize -= minSize; auto compGain = [&](HyperGraph::Vertex* lhs, HyperGraph::Vertex* rhs)->bool {return traitTable[lhs].Gain < traitTable[rhs].Gain; }; // 直到所有 Vertex 都被計算過 while (1) { // O(n) 尋找左/右子集中 Gain 值最大的 Vertex // 當所有 Vertex 都被標記為 Sorted 時,結束迴圈 HyperGraph::Vertex* lMax = nullptr, * rMax = nullptr; int lsize = 0, rsize = 0; int curLmax = 0, curRmax = 0; bool finish = true; for (auto t : traitTable) { if (!t.second.Sorted)finish = false; if (t.second.IsLHS) { lsize++; if (t.second.Sorted) continue; if ((lMax == nullptr) || (t.second.Gain > curLmax)) { lMax = t.first; curLmax = t.second.Gain; } } else { rsize++; if (t.second.Sorted) continue; if ((rMax == nullptr) || (t.second.Gain > curRmax)) { rMax = t.first; curRmax = t.second.Gain; } } } if (finish) break; // T: R2L, F: L2R // 判斷要進行移動的 Vertex bool moveSide; HyperGraph::Vertex* toMove = nullptr; if (lMax == nullptr) { moveSide = true; toMove = rMax; } else if (rMax == nullptr) { moveSide = false; toMove = lMax; } else { float lgain = traitTable[lMax].Gain, rgain = traitTable[rMax].Gain; // 當左右 Gain 值都 <= 0 時,提前結束演算 // (意即此時移動任何節點,都是無/負收益) if ((lgain <= 0) && (rgain <= 0)) break; if (lsize < minSize) moveSide = true; // 左子集過小 else if (lsize > maxSize) moveSide = false; // 左子集過大 else if (lgain == rgain) moveSide = (lsize < rsize); // 效益相等,選較小的子集 else moveSide = (lgain < rgain); // 選取效益較佳的子集 toMove = (moveSide ? rMax : lMax); #ifdef DEBUG cout << "gain LHS: " << lgain << ", RHS: " << rgain << '\n'; if (moveSide) cout << "move " << ((toMove->Name == "") ? to_string(toMove->ID) : toMove->Name) << " to Left\n"; else cout << "move " << ((toMove->Name == "") ? to_string(toMove->ID) : toMove->Name) << " to Right\n"; #endif // DEBUG } // 移動節點 traitTable[toMove].IsLHS = !traitTable[toMove].IsLHS; // 重新計算可能被影響到的 Gain 值 for (auto v : toMove->GetNeighbors()) { calGain(v); } // 標記被移動的節點為 Sorted traitTable[toMove].Sorted = true; } // 演算結束後,根據標記 insert result for (auto pair : traitTable) { if (pair.second.IsLHS) LHS.insert(pair.first); else RHS.insert(pair.first); } // 計算那些超邊為 Cut Edge for (auto e : Source.EdgesMap()) { auto vertices = e.second->ConnectedVertices(); if (vertices.empty()) continue; bool side = traitTable.at(*vertices.begin()).IsLHS; for (auto v : vertices) { if (side != traitTable[v].IsLHS) { CutEdges.insert(e.second); break; } } } traitTable.clear(); } FiducciaMattheyses::~FiducciaMattheyses() { } void FiducciaMattheyses::calGain(HyperGraph::Vertex* vertex) { float& gain = traitTable[vertex].Gain; gain = 0; int eCount = 0; float totalDiff = 0; float totalSame = 0; for (auto e : vertex->ConnectedEdges()) { // 此處的 Gain( v(i)) 值定義 // 1. 每當有一 e(j) 包含 v(i),且 e(j) 中沒有其他 v 在 v(i) 的分區: gain +1 // 即為若移動此節點,會減少一個 Cut Edge // 2. 每當有一 e(j) 包含 v(i),且 e(j) 中所有的 v 都在 v(i) 的分區: gain -1 // 即為若移動此節點,會增加一個 Cut Edge // 3. 次級條件,gain += (同分區鄰居數量 - 不同分區鄰居數量) * con3step (使其總和不超過 +-1) // 即為盡量使同邊節點聚集 bool side = traitTable[vertex].IsLHS; int same = -1, diff = 0; for (auto v : e->ConnectedVertices()) { if (traitTable[v].IsLHS == side) same++; else diff++; } if (same == 0) gain++; if (diff == 0) gain--; eCount++; totalDiff += diff; totalSame += same; } gain += con3step * (totalDiff - totalSame) / eCount; #ifdef DEBUG cout << "Gain of " << ((vertex->Name == "") ? to_string(vertex->ID) : vertex->Name) << " is " << traitTable[vertex].Gain << '\n'; #endif // DEBUG } #pragma endregion #pragma region ostream std::ostream& operator<<(std::ostream& os, HyperGraph& obj) { bool deep = ((obj.VerticesMap().size() <= 100) && (obj.EdgesMap().size() <= 100)); os << "HyperGraph:\n" << "- Vertices * " << obj.verticesMap.size() << " :\n"; if (deep) for (auto pair : obj.verticesMap) os << *pair.second; os << "- Edges * " << obj.edgesMap.size() << " :\n"; if (deep) for (auto pair : obj.edgesMap) os << *pair.second; return os; } std::ostream& operator<<(std::ostream& os, HyperGraph::Vertex& obj) { os << "v: "; if (obj.Name != "") os << obj.Name; else os << obj.ID; os << " -> "; for (auto e : obj.connectedEdges) os << ((e->Name == "") ? to_string(e->ID) : e->Name) << ", "; os << '\n'; return os; } std::ostream& operator<<(std::ostream& os, HyperGraph::Edge& obj) { os << "e: "; if (obj.Name != "") os << obj.Name; else os << obj.ID; os << " -> "; for (auto v : obj.connectedVertices) os << ((v->Name == "") ? to_string(v->ID) : v->Name) << ", "; os << '\n'; return os; } std::ostream& operator<<(std::ostream& os, FiducciaMattheyses& obj) { bool deep = obj.Source.VerticesMap().size() < 100; os << "FiducciaMattheyses:\n"; os << "- LHS * " << obj.LHS.size() << ":\n"; if (deep) { for (auto v : obj.LHS) { os << *v; } } os << "- RHS * " << obj.RHS.size() << ":\n"; if (deep) { for (auto v : obj.RHS) { os << *v; } } os << "- Cut Edges * " << obj.CutEdges.size() << ":\n"; if (deep) { for (auto e : obj.CutEdges) { os << *e; } } return os; } #pragma endregion ``` > [name=Naive Ys]Last updated : 2023/09/23