Weisfeiler-Leman Hierarchy
Key Points
- •The Weisfeiler–Leman (WL) hierarchy is a family of color-refinement procedures that iteratively color vertices (or k-tuples of vertices) to capture graph structure for isomorphism testing.
- •1-WL (color refinement) updates each vertex color from the multiset of its neighbors’ colors and stabilizes in at most n − 1 rounds.
- •k-WL generalizes this by coloring ordered k-tuples and looking at how the color changes when one coordinate is replaced by every vertex, which greatly increases distinguishing power but also costs memory.
- •Two graphs that end with the same WL color histograms are indistinguishable by that k-WL level, but they may still be non-isomorphic.
- •1-WL runs in roughly O(ℓ (n + m) log n) time and O(n + m) space, where ℓ is the number of refinement rounds until stabilization.
- •k-WL for typically costs O(ℓ k ) time and O() space, so it is only practical for small k and small graphs.
- •WL colors are isomorphism invariants and are widely used as fast filters in graph isomorphism solvers and as features in machine learning on graphs.
- •There are known counterexamples (e.g., Cai–Fürer–Immerman graphs) where even high-dimensional WL fails, so WL is powerful but not complete.
Prerequisites
- →Graphs and adjacency representations — WL operates on graphs; you must understand vertices, edges, neighborhoods, and adjacency lists/matrices.
- →Hashing and maps — WL uses signatures mapped to integer colors; efficient and deterministic hashing is essential.
- →Time and space complexity — k-WL costs grow as n^k; understanding asymptotics guides feasible choices of k.
- →Partitions and equivalence relations — 1-WL produces equitable partitions; reasoning about refinement and fixed points uses these ideas.
- →Basic finite model theory (optional) — WL connects to counting logic; this deepens theoretical understanding but is not required for coding.
Detailed Explanation
Tap terms for definitions01Overview
The Weisfeiler–Leman (WL) hierarchy is a family of increasingly powerful algorithms for distinguishing non-isomorphic graphs. The simplest member, 1-WL—also called color refinement—assigns a color (an integer label) to each vertex and repeatedly refines these colors by summarizing the multiset of colors of each vertex’s neighbors. This process continues until the coloring stabilizes (no change occurs). If two graphs stabilize to different color histograms, they are certainly non-isomorphic; if their histograms match, they are indistinguishable by 1-WL (but not guaranteed to be isomorphic). The k-dimensional version, k-WL, operates on ordered k-tuples of vertices. It refines the color of each tuple using the colors of all tuples obtained by replacing one coordinate with every vertex in the graph. This greatly increases expressiveness and can distinguish many graphs that 1-WL cannot. However, the computational cost grows steeply with k because there are n^k tuples. WL methods are central in theory (connections to finite model theory and counting logic) and practice (fast graph isomorphism heuristics, graph kernels, and features for graph neural networks). They provide a tunable trade-off between power and cost: higher k gives more distinguishing power but quickly becomes expensive.
02Intuition & Analogies
Imagine every vertex is a house in a city, initially painted the same color. In the first round, each house repaints itself based on the colors of the houses on its street—perhaps by writing down, in sorted order, the colors it sees and then hashing that list to a new paint color. Houses on streets with the same patterns end up with the same new color. In the next round, they do this again, now using the new colors they and their neighbors have. Over time, neighborhoods that are structurally different (e.g., different degree patterns or arrangements) settle on different colors. When no house changes color anymore, the process stabilizes: that’s 1-WL. If two cities end with different final color histograms, they clearly are not the same; but if the histograms match, they could still be different cities that happen to fool this particular coloring rule. Now zoom out to k-WL: instead of painting individual houses, we paint small ordered teams of k houses. To refine a team’s color, we perform a thought experiment: for each position in the team, we try replacing that house with every other house in the city and look at the resulting teams’ colors. This is like stress-testing how the team relates to the city from each member’s perspective. Collecting all these colors yields a rich signature that better captures the city’s layout. Increasing k is like upgrading from a blurry snapshot (k = 1) to a higher-resolution camera (k ≥ 2). You see more structure, but each photo takes far more storage and processing. Eventually, you hit practical limits: even though the camera could, in principle, resolve most differences, the cost becomes prohibitive. That’s the key trade-off behind the WL hierarchy: more detail versus more computation.
03Formal Definition
04When to Use
Use 1-WL as a fast, memory-light prefilter in graph isomorphism pipelines: it quickly separates many non-isomorphic graphs by degree and local neighborhood patterns. It is also a strong baseline feature for tasks like graph classification or clustering: the stabilized color histogram (or per-vertex colors) provides a structural fingerprint. When initial vertex/edge labels exist (e.g., atom types in molecules), include them in the initial coloring to respect domain-specific information. Use k-WL with small k (typically k = 2 or 3) when 1-WL is too weak—for example, distinguishing many regular graphs that 1-WL cannot tell apart, or when richer features are required for learning tasks. 2-WL often offers a substantial power jump at acceptable cost for small and medium graphs (n in the low thousands for well-engineered implementations, often much less for naive ones). Avoid large k unless graphs are tiny: time and memory blow up as n^k. Do not rely on WL as a complete isomorphism test: there are families of non-isomorphic graphs (e.g., Cai–Fürer–Immerman) that defeat every fixed k. Instead, combine WL with backtracking, individualization-refinement, or canonical labeling for exact isomorphism. In ML settings, WL features (and WL-inspired GNNs) are strong but have expressivity limits matching their WL level.
⚠️Common Mistakes
• Non-deterministic hashing: Using unordered maps without canonical relabeling can assign different color IDs across runs or across graphs, breaking cross-graph comparisons. Fix by rebuilding color IDs each round using a consistent, order-independent mapping from signatures to consecutive integers, or by sharing the same palette across both graphs in each round. • Mixing labels incorrectly: If your input has initial labels (e.g., types), forgetting to include them in the initial coloring (or in signatures) loses crucial information; conversely, adding degree into the initial color changes the formal 1-WL variant. Be explicit about what your initial colors encode. • Early-stopping errors: Declaring graphs indistinguishable because current round histograms match, without running to stabilization, can yield false conclusions. Always iterate until no color changes occur in either graph (or until a mismatch appears). • Memory blow-ups in k-WL: Storing explicit signatures as vectors of length k·n per tuple is straightforward but quickly becomes infeasible. Use compressed hashing or counting and keep k small; profile memory. • Ignoring graph direction/multigraphs: WL formulas and code must be adapted for directed graphs, edge labels, or multiedges. Ensure signatures reflect the graph model you intend. • Comparing raw color IDs across separate runs: Even with deterministic mapping, color integers are arbitrary labels. For equivalence, compare frequency histograms (or distributions) of colors, not the exact integers, unless colors were assigned using a shared palette in the same round.
Key Formulas
1-WL Update
Explanation: The next color of vertex v is a function of its current color and the multiset of its neighbors’ current colors. The multiset keeps multiplicities, so repeated neighbor colors matter.
Stabilization Condition
Explanation: The process stops when applying the update rule yields the same coloring as before. At this fixed point, the coloring is equitable.
k-WL Update
Explanation: For each k-tuple, we look at all tuples formed by replacing one coordinate by every vertex and aggregate their colors. This captures richer relational structure than 1-WL.
Number of k-Tuples
Explanation: There are n choices for each position in a k-tuple, so the space of tuples grows exponentially with k. This drives the memory and time cost of k-WL.
1-WL Complexity
Explanation: Each round processes all vertices and incident edges and sorts or hashes neighbor-color multisets. The factor is the number of rounds until stabilization (at most n − 1, usually much smaller).
k-WL Complexity
Explanation: For each of tuples, we examine k replacement positions and n replacements, leading to k operations per round. We store one color per tuple, so memory is .
Round Bound
Explanation: 1-WL refines a partition of V and can strictly increase the number of color classes at most n − 1 times. Thus stabilization occurs within n − 1 rounds.
1-WL Indistinguishability Test
Explanation: If the final color histograms match, the graphs are indistinguishable by 1-WL. This is necessary for isomorphism but not sufficient.
Complexity Analysis
Code Examples
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Hash for vector<int> to use as a key in unordered_map 5 struct VecHash { 6 size_t operator()(const vector<int>& v) const noexcept { 7 // 64-bit splitmix-like hash combine 8 uint64_t h = 1469598103934665603ull; // FNV offset 9 for (int x : v) { 10 uint64_t y = static_cast<uint64_t>(x) + 0x9e3779b97f4a7c15ull; 11 h ^= y; 12 h *= 1099511628211ull; // FNV prime 13 } 14 return static_cast<size_t>(h); 15 } 16 }; 17 18 // Perform 1-WL color refinement until stabilization. 19 // adj: undirected adjacency list (0..n-1) 20 // init_colors: optional initial colors; if empty, all zeros. 21 vector<int> oneWL_color_refinement(const vector<vector<int>>& adj, const vector<int>& init_colors = {}) { 22 int n = (int)adj.size(); 23 vector<int> color(n, 0); 24 if (!init_colors.empty()) color = init_colors; 25 // Optionally include degree in the first iteration by seeding with (label, degree) 26 // to accelerate convergence while preserving correctness for labeled graphs. 27 28 vector<int> new_color(n, 0); 29 30 while (true) { 31 // Build signatures for all vertices: (old_color, sorted multiset of neighbor colors) 32 vector<vector<int>> signatures(n); 33 signatures.reserve(n); 34 for (int v = 0; v < n; ++v) { 35 vector<int> sig; 36 sig.push_back(color[v]); 37 // Collect neighbor colors 38 sig.reserve(1 + (int)adj[v].size()); 39 for (int u : adj[v]) sig.push_back(color[u]); 40 sort(sig.begin() + 1, sig.end()); // sort only neighbor colors 41 signatures[v] = move(sig); 42 } 43 // Canonically relabel signatures to consecutive colors 44 unordered_map<vector<int>, int, VecHash> id; 45 id.reserve(n * 2); 46 int next_id = 0; 47 for (int v = 0; v < n; ++v) { 48 auto it = id.find(signatures[v]); 49 if (it == id.end()) { 50 id.emplace(signatures[v], next_id); 51 new_color[v] = next_id++; 52 } else { 53 new_color[v] = it->second; 54 } 55 } 56 if (new_color == color) break; // stabilized 57 color.swap(new_color); 58 } 59 return color; 60 } 61 62 int main() { 63 ios::sync_with_stdio(false); 64 cin.tie(nullptr); 65 66 // Example: Two triangles connected by a bridge (6 vertices) 67 int n = 6; 68 vector<vector<int>> adj(n); 69 auto add_edge = [&](int u, int v){ adj[u].push_back(v); adj[v].push_back(u); }; 70 71 add_edge(0,1); add_edge(1,2); add_edge(2,0); // triangle 0-1-2 72 add_edge(3,4); add_edge(4,5); add_edge(5,3); // triangle 3-4-5 73 add_edge(2,3); // bridge 74 75 for (int v = 0; v < n; ++v) sort(adj[v].begin(), adj[v].end()); 76 77 vector<int> colors = oneWL_color_refinement(adj); 78 79 cout << "Final 1-WL colors:\n"; 80 for (int v = 0; v < n; ++v) { 81 cout << "v=" << v << " color=" << colors[v] << "\n"; 82 } 83 return 0; 84 } 85
This program implements 1-WL (color refinement). Each round forms a vertex signature comprising its current color and the sorted multiset of neighbor colors. Signatures are mapped to consecutive integers to produce new colors. The process repeats until no vertex changes color. The output colors partition vertices into classes with identical local structure as seen by 1-WL.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct VecHash { size_t operator()(const vector<int>& v) const noexcept { 5 uint64_t h = 1469598103934665603ull; for (int x : v){ uint64_t y = (uint64_t)x + 0x9e3779b97f4a7c15ull; h ^= y; h *= 1099511628211ull; } return (size_t)h; } }; 6 7 // One round of 1-WL on two graphs using a shared signature->color palette. 8 static void one_round_shared(const vector<vector<int>>& A, const vector<int>& cA, 9 const vector<vector<int>>& B, const vector<int>& cB, 10 vector<int>& nA, vector<int>& nB) { 11 int n = (int)A.size(); 12 // Build signatures for both graphs 13 vector<vector<int>> sigA(n), sigB(n); 14 for (int v = 0; v < n; ++v) { 15 vector<int> s; s.push_back(cA[v]); 16 for (int u : A[v]) s.push_back(cA[u]); 17 sort(s.begin() + 1, s.end()); 18 sigA[v] = move(s); 19 } 20 for (int v = 0; v < n; ++v) { 21 vector<int> s; s.push_back(cB[v]); 22 for (int u : B[v]) s.push_back(cB[u]); 23 sort(s.begin() + 1, s.end()); 24 sigB[v] = move(s); 25 } 26 // Shared palette: same signature gets the same ID across both graphs in this round 27 unordered_map<vector<int>, int, VecHash> id; id.reserve(2*n); 28 int next_id = 0; 29 for (int v = 0; v < n; ++v) { 30 auto it = id.find(sigA[v]); 31 if (it == id.end()) { id.emplace(sigA[v], next_id); nA[v] = next_id++; } 32 else { nA[v] = it->second; } 33 } 34 for (int v = 0; v < n; ++v) { 35 auto it = id.find(sigB[v]); 36 if (it == id.end()) { id.emplace(sigB[v], next_id); nB[v] = next_id++; } 37 else { nB[v] = it->second; } 38 } 39 } 40 41 // Return true if graphs are indistinguishable by 1-WL (same stabilized color histogram) 42 bool indistinguishable_1WL(const vector<vector<int>>& A, const vector<vector<int>>& B, 43 const vector<int>& initA = {}, const vector<int>& initB = {}) { 44 int n = (int)A.size(); if ((int)B.size() != n) return false; 45 vector<int> cA(n,0), cB(n,0); if (!initA.empty()) cA = initA; if (!initB.empty()) cB = initB; 46 vector<int> nA(n,0), nB(n,0); 47 while (true) { 48 one_round_shared(A, cA, B, cB, nA, nB); 49 // Early rejection: compare histograms this round 50 unordered_map<int,int> hA, hB; hA.reserve(n); hB.reserve(n); 51 for (int x : nA) ++hA[x]; for (int x : nB) ++hB[x]; 52 if (hA != hB) return false; 53 if (nA == cA && nB == cB) return true; // stabilized with matching histograms 54 cA.swap(nA); cB.swap(nB); 55 } 56 } 57 58 int main(){ 59 ios::sync_with_stdio(false); 60 cin.tie(nullptr); 61 62 // Build a graph and a permuted isomorphic copy 63 int n = 6; 64 vector<vector<int>> G(n), H(n); 65 auto add = [&](vector<vector<int>>& adj, int u, int v){ adj[u].push_back(v); adj[v].push_back(u); }; 66 67 // G: square 0-1-2-3-0 with diagonals 0-2 and 1-3 (K4), plus two isolated vertices 4,5 connected as edge 4-5 68 add(G,0,1); add(G,1,2); add(G,2,3); add(G,3,0); add(G,0,2); add(G,1,3); add(G,4,5); 69 for (int i=0;i<n;++i) sort(G[i].begin(), G[i].end()); 70 71 // Permute vertices: p = [2,3,0,1,5,4] 72 vector<int> p = {2,3,0,1,5,4}; 73 vector<int> inv(n); for (int i=0;i<n;++i) inv[p[i]] = i; 74 H.assign(n, {}); 75 for (int u=0;u<n;++u) for (int v: G[u]) if (u < v) { add(H, p[u], p[v]); } 76 for (int i=0;i<n;++i) sort(H[i].begin(), H[i].end()); 77 78 bool same = indistinguishable_1WL(G, H); 79 cout << (same ? "Indistinguishable by 1-WL (likely isomorphic).\n" : "Distinguished by 1-WL (non-isomorphic).\n"); 80 81 return 0; 82 } 83
This code refines two graphs in lockstep using a shared signature-to-color map each round. Sharing the palette guarantees that identical signatures in either graph receive the same color ID, making cross-graph histogram comparisons meaningful. The function returns true if, after stabilization, both graphs have identical color histograms (i.e., they are indistinguishable by 1-WL).
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct VecHash { size_t operator()(const vector<int>& v) const noexcept { 5 uint64_t h = 1469598103934665603ull; for (int x : v){ uint64_t y = (uint64_t)x + 0x9e3779b97f4a7c15ull; h ^= y; h *= 1099511628211ull; } return (size_t)h; } }; 6 7 // Convert between tuple index and vector<int> tuple under base-n encoding 8 static size_t tuple_to_index(const vector<int>& t, int n){ size_t idx=0; for (int x: t){ idx = idx * (size_t)n + (size_t)x; } return idx; } 9 static vector<int> index_to_tuple(size_t idx, int n, int k){ vector<int> t(k); for (int i=k-1;i>=0;--i){ t[i] = (int)(idx % (size_t)n); idx /= (size_t)n; } return t; } 10 11 // Build initial color of a k-tuple from equality pattern and pairwise adjacency among tuple entries 12 static vector<int> build_initial_colors_kWL(const vector<vector<int>>& adj, int k){ 13 int n = (int)adj.size(); 14 vector<vector<char>> A(n, vector<char>(n, 0)); 15 for (int u=0; u<n; ++u) for (int v: adj[u]) A[u][v] = 1; 16 size_t NK = 1; for (int i=0;i<k;++i) NK *= (size_t)n; 17 vector<int> color(NK, 0); 18 unordered_map<vector<int>, int, VecHash> id; id.reserve(NK/2 + 1); 19 int next_id = 0; 20 vector<int> key; key.reserve(k + k*k); 21 for (size_t idx=0; idx<NK; ++idx){ 22 vector<int> t = index_to_tuple(idx, n, k); 23 key.clear(); 24 // equality pattern: for each pair (i,j), record 1 if t[i]==t[j], else 0 25 for (int i=0;i<k;++i) for (int j=0;j<k;++j) key.push_back(t[i]==t[j]); 26 // adjacency pattern among tuple entries (undirected; include both (i,j) entries for simplicity) 27 for (int i=0;i<k;++i) for (int j=0;j<k;++j) key.push_back(A[t[i]][t[j]]); 28 auto it = id.find(key); 29 if (it == id.end()) { id.emplace(key, next_id); color[idx] = next_id++; } 30 else { color[idx] = it->second; } 31 } 32 return color; 33 } 34 35 // One educational (naive) round of k-WL: builds huge deterministic signatures 36 static vector<int> kWL_round(const vector<vector<int>>& adj, const vector<int>& prev, int k){ 37 int n = (int)adj.size(); 38 size_t NK = prev.size(); 39 unordered_map<vector<int>, int, VecHash> id; id.reserve(NK/2 + 1); 40 vector<int> next(prev.size()); 41 vector<int> key; key.reserve(1 + k * n); 42 43 for (size_t idx=0; idx<NK; ++idx){ 44 vector<int> t = index_to_tuple(idx, n, k); 45 key.clear(); 46 key.push_back(prev[idx]); // include old color 47 // For each position i, append colors of tuples with t[i] replaced by all w in V in fixed order (0..n-1) 48 for (int i=0;i<k;++i){ 49 for (int w=0; w<n; ++w){ 50 int old = t[i]; 51 t[i] = w; 52 size_t jdx = tuple_to_index(t, n); 53 key.push_back(prev[jdx]); 54 t[i] = old; // restore 55 } 56 } 57 auto it = id.find(key); 58 if (it == id.end()) { int nid = (int)id.size(); id.emplace(key, nid); next[idx] = nid; } 59 else { next[idx] = it->second; } 60 } 61 return next; 62 } 63 64 // Run k-WL until stabilization. WARNING: Only feasible for very small n and k. 65 vector<int> kWL_refine(const vector<vector<int>>& adj, int k, int max_rounds = 100) { 66 int n = (int)adj.size(); 67 // Initial colors from equality + adjacency pattern 68 vector<int> color = build_initial_colors_kWL(adj, k); 69 for (int round=0; round<max_rounds; ++round){ 70 vector<int> next = kWL_round(adj, color, k); 71 if (next == color) break; // stabilized 72 color.swap(next); 73 } 74 return color; 75 } 76 77 int main(){ 78 ios::sync_with_stdio(false); 79 cin.tie(nullptr); 80 81 // Tiny example: 4-cycle C4 82 int n = 4; vector<vector<int>> adj(n); 83 auto add=[&](int u,int v){ adj[u].push_back(v); adj[v].push_back(u); }; 84 add(0,1); add(1,2); add(2,3); add(3,0); 85 for (int i=0;i<n;++i) sort(adj[i].begin(), adj[i].end()); 86 87 int k = 2; // 2-WL on ordered pairs 88 vector<int> colors2 = kWL_refine(adj, k); 89 cout << "Number of distinct 2-WL colors on V^2: " 90 << unordered_set<int>(colors2.begin(), colors2.end()).size() << "\n"; 91 92 return 0; 93 } 94
This educational 2/3/…-WL implementation explicitly builds deterministic signatures by listing, for each tuple, the colors of all tuples obtained by replacing one coordinate with every vertex in order. It also initializes tuple colors using equality and pairwise adjacency among tuple coordinates. The code is correct but extremely costly: it is meant for tiny graphs and for learning purposes.