We are given an undirected simple graph $ G = (V, E) $ with $ n = |V| $ vertices and $ m = |E| $ edges, stored as an adjacency list. Since the graph is undirected, each edge $ \{u, v\} $ appears twice: once as $ v $ in $ u $’s list and once as $ u $ in $ v $’s list. These two entries are called twins, and the goal is to set a twin pointer from each entry to its twin.
A brute-force method searching for the twin by scanning the neighbor’s list could take $ O(\deg(v)) $ per entry, leading to $ O(m^2) $ time in the worst case. However, we can achieve optimal performance using a hash table.
Efficient Algorithm Using a Hash Table:
We process each of the $ 2m $ adjacency list entries exactly once. For an entry representing the directed pair $ (u, v) $, we create a canonical key for the undirected edge:
$$
k = (\min(u,v),\ \max(u,v))
$$
This ensures that both $ (u,v) $ and $ (v,u) $ map to the same key.
We maintain a hash table that maps each key $ k $ to the first adjacency-list node seen for that edge. When we encounter the second occurrence (the twin), we link the two nodes with twin pointers.
The steps are:
- Initialize an empty hash table $ H $.
- For each vertex $ u \in V $:
- For each neighbor $ v $ in $ u $’s adjacency list:
- Let $ k = (\min(u,v), \max(u,v)) $.
- If $ k \notin H $, store a pointer to this list node in $ H[k] $.
- Else, retrieve the stored node from $ H[k] $, and set mutual twin pointers between the two nodes.
Each operation (hash lookup, insertion, pointer assignment) takes $ O(1) $ expected time. The total number of entries processed is $ 2m $, and we also iterate over $ n $ vertices to access their lists.
Thus, the total time complexity is $ \Theta(n + m) $.
Example of Hash Table Usage
Consider a graph with edges $ \{1,2\}, \{2,3\} $. The adjacency lists are:
- Vertex 1: [2]
- Vertex 2: [1, 3]
- Vertex 3: [2]
As we process each entry, the hash table evolves as follows:
$$
\begin{array}{|c|c|c|}
\hline
\text{Step} & \text{Entry Processed} & \text{Hash Table } H \\
\hline
1 & (1,2) & \{ (1,2) \mapsto \text{node}_{1\to2} \} \\
2 & (2,1) & \text{Twin found! Link } \text{node}_{2\to1} \leftrightarrow \text{node}_{1\to2} \\
3 & (2,3) & \{ (1,2) \mapsto \cdots,\ (2,3) \mapsto \text{node}_{2\to3} \} \\
4 & (3,2) & \text{Twin found! Link } \text{node}_{3\to2} \leftrightarrow \text{node}_{2\to3} \\
\hline
\end{array}
$$
After processing all entries, every adjacency list node has its twin pointer correctly set.
The algorithm runs in linear time relative to the input size, which is $ \Theta(n + m) $. This is optimal, as we must examine every vertex and every edge at least once.
$$
\boxed{\text{B. } \Theta(n + m)}
$$