The source example: three fully connected nodes A, B, C with features h_A = [1, 0], h_B = [0, 1], h_C = [1, 1]. Every node has degree 2, so A_norm is the adjacency matrix with each row divided by 2.
A_norm = [[0, ½, ½], H = [[1, 0],
[½, 0, ½], [0, 1],
[½, ½, 0]] [1, 1]]
round 1: A_norm @ H = [[0.5, 1.0], ← A averaged h_B and h_C
[1.0, 0.5], ← B averaged h_A and h_C
[0.5, 0.5]] ← C averaged h_A and h_B
with W = [[1, 0.5], [0.5, 1]] and ReLU:
h' A = [0.5, 1.0] @ W = [0.5·1 + 1.0·0.5, 0.5·0.5 + 1.0·1] = [1.0, 1.25]
(same idea for B and C; C lands on [0.75, 0.75])
round 2 with W = identity, to keep the arithmetic visible:
A_norm @ [[0.5, 1.0], [1.0, 0.5], [0.5, 0.5]]
= [[0.75, 0.5], ← A now mixes in information from B's neighbours
[0.5, 0.75], ← i.e. from its 2-hop neighbourhood
[0.75, 0.75]]
after one round a node knows its 1-hop neighbours; after K rounds, K hops.
Self-loops and symmetric normalization. The classic GCN adds the node’s own feature by setting  = A + I, then divides each entry by √(dᵢdⱼ) instead of the row sum: A_norm = D̂^(−1/2)  D̂^(−1/2). On the triangle every degree becomes 3, so the operator is a plain average over the closed neighbourhood — a node keeps a third of itself:
closed neighbourhood of A = {A, B, C}, so
(after W = identity and ReLU)
h' A = ([1, 0] + [0, 1] + [1, 1]) / 3 = [2/3, 2/3] ✓
symmetric normalization keeps the operator's eigenvalues in [−1, 1],
so stacked layers cannot blow up; it is one matrix away from the
normalized Laplacian: L_sym = I − D^(−1/2) A D^(−1/2).
Why not just sum the neighbours? A hub with 1,000 neighbours would hand back a vector 1,000× larger than a leaf, and after a few layers the largest-degree node would dominate every number. Dividing by degree — and symmetrically by √(dᵢdⱼ) — makes every node’s update comparable, the same trick normalization layers use elsewhere.