Three-Reversal Involution vs. $\gcd(N, k)$ Juggling Cycles
Consider rotating an array $A = [X \;\vert\; Y]$ so that a prefix $X$ of length $n - k$ and a suffix $Y$ of length $k$ swap positions to become $Y X$ (a right rotation by $k \equiv k \bmod n$).In abstract algebra, reversing an array is an involution ($R(R(A)) = A$) that reverses the order of concatenated blocks: $(X Y)^R = Y^R X^R$. Therefore, reversing $X$ and $Y$ individually and then reversing the whole array (or reversing the whole array first, then reversing the first $k$ and last $n-k$ elements) yields:
$$\left( X^R Y^R \right)^R = (Y^R)^R (X^R)^R = Y X$$
Alternatively, consider Bentley's Juggling Algorithm: moving each element directly from index $i$ to $(i + k) \bmod n$ traces out cyclic orbits under the subgroup generated by $+k$ in $\mathbb{Z}_n$. By elementary group theory (Lagrange's theorem), the subgroup $\langle k \rangle \subseteq \mathbb{Z}_n$ has size $L = \frac{n}{\gcd(n, k)}$ and partitions the $n$ indices into exactly $g = \gcd(n, k)$ disjoint cycles, each of length $n / g$! Starting at each $s \in [0, g - 1]$ and chasing the cycle moves every element exactly once ($N$ writes vs. $3N$ reads/writes in 3-Reversal).
The symmetries of a square $N \times N$ grid form the dihedral group $D_4$ of order 8, generated by two reflections: diagonal transposition $T(r, c) = (c, r)$ and horizontal row reversal $H(r, c) = (r, N - 1 - c)$. Their composition produces a $90^\circ$ clockwise rotation:
$$(H \circ T)(r, c) = H(c, r) = (c,\; N - 1 - r)$$
Conversely, reversing each row first and then transposing ($T \circ H$) produces a $90^\circ$ counter-clockwise rotation $(N - 1 - c, r)$. What about transposing a non-square $R \times C$ matrix stored in a 1D row-major buffer of size $R \cdot C$? An element at $(r, c)$ starts at index $i = rC + c$ and moves to $(c, r)$ at index $i' = cR + r$. Observe that for every $i < RC - 1$:
$$i' = cR + r = cR + \lfloor i / C \rfloor \equiv R \cdot i \pmod{RC - 1}$$
Thus in-place rectangular transposition is simply cycle-following on the multiplicative permutation $i \mapsto (R \cdot i) \bmod (RC - 1)$!
Comparison of In-Place Rotation & Transposition Algorithms
| Algorithm | Time Complexity | Element Moves | Cache Locality Profile | Best Use Case |
|---|---|---|---|---|
| 3-Reversal Rotation | $O(N)$ | $\approx 1.5 N$ swaps ($3N$ writes) | Sequential stride-1 (L1 prefetcher optimal) | Contiguous arrays in RAM (std::rotate on random-access) |
| Juggling ($gcd(N, k)$ Cycles) | $O(N)$ | $N + \gcd(N, k)$ moves | Stride-$k$ jumps (TLB/cache thrashing for large $k$) | Large objects where move construction is expensive |
| Gries-Mills Block Swap | $O(N)$ | $\approx 2N$ moves | Sequential block swaps | Forward iterators without random access |
| Cache-Tiled $B \times B$ Transpose | $O(N^2)$ | $N^2$ moves | Zero conflict misses when $2 B^2 \cdot \text{sizeof}(T) \le \text{L1d}$ | Large matrices ($N \ge 512$) |
Visualizing 3-Reversal Rotation & $D_4$ Matrix Transpose + Reflect
Juggling Cycle Decomposition ($gcd(N, k)$ Orbits)
g = std::gcd(N, k)"] --> B["For start = 0 .. g - 1
tmp = A[start], cur = start"] B --> C["next = (cur + N - k) % N
(Pull-chain for right rotate)"] C --> D{"next == start?"} D -- "No" --> E["A[cur] = move(A[next])
cur = next"] E --> C D -- "Yes (Cycle closed)" --> F["A[cur] = move(tmp)"]
Even though Juggling performs $N$ writes while 3-Reversal performs $3N$ writes, benchmarked on $N = 10^7$ int32_t elements in DRAM, 3-Reversal is $2.5\times$ to $4\times$ faster than Juggling! Why? Because 3-Reversal accesses memory strictly sequentially (stride $pm 1$) and can be vectorized with AVX2 _mm256_permutevar8x32_epi32, utilizing all 64 bytes of every fetched cache line. Juggling jumps by stride $k$; when $k \ge 16$, every single read/write touches a different 64-byte cache line and different 4 KB virtual page, causing catastrophic L1d and dTLB thrashing. Similarly, naive $N \times N$ matrix transposition reads column $A[0 \dots N-1][c]$ at stride $N$: when $N = 1024$ (a power of two), every row is separated by $4096$ bytes, mapping to the exact same L1d cache set (8-way set-associative conflict misses). Processing the matrix in $32 \times 32$ cache-resident tiles completely eliminates these conflict misses.
Trace 2D prefix-sum inclusion-exclusion P[r2][c2] - P[r1-1][c2] - P[r2][c1-1] + P[r1-1][c1-1], O(1) difference-array range updates, and Kadane circular max subarray.
Invariant & State
Production Implementation: 3-Reversal, Juggling & Cache-Tiled Square Transpose
#include <algorithm>
#include <cstdint>
#include <numeric>
#include <utility>
#include <vector>
// Cache-friendly 3-Reversal Right Rotation by k positions
template <typename T>
void rotate_right_reversal(std::vector<T>& a, std::size_t k) noexcept {
const std::size_t n = a.size();
if (n <= 1) return;
k %= n;
if (k == 0) return;
std::reverse(a.begin(), a.end());
std::reverse(a.begin(), a.begin() + static_cast<std::ptrdiff_t>(k));
std::reverse(a.begin() + static_cast<std::ptrdiff_t>(k), a.end());
}
// Minimum-Move Juggling Left Rotation by k positions using gcd(n, k) cycles
template <typename T>
void rotate_left_juggling(std::vector<T>& a, std::size_t k) noexcept {
const std::size_t n = a.size();
if (n <= 1) return;
k %= n;
if (k == 0) return;
const std::size_t cycles = std::gcd(n, k);
for (std::size_t start = 0; start < cycles; ++start) {
T tmp = std::move(a[start]);
std::size_t cur = start;
while (true) {
std::size_t next = cur + k;
if (next >= n) next -= n; // faster than % n!
if (next == start) break;
a[cur] = std::move(a[next]);
cur = next;
}
a[cur] = std::move(tmp);
}
}
// In-Place 90-Degree Clockwise Matrix Rotation (Cache-Tiled Transpose + Row Reverse)
void rotate_matrix_90_cw(std::vector<int>& mat, std::size_t n) noexcept {
constexpr std::size_t TILE = 32;
// Step 1: Tiled In-Place Transpose across upper triangle (r < c)
for (std::size_t r0 = 0; r0 < n; r0 += TILE) {
for (std::size_t c0 = r0; c0 < n; c0 += TILE) {
const std::size_t r_end = std::min(r0 + TILE, n);
const std::size_t c_end = std::min(c0 + TILE, n);
for (std::size_t r = r0; r < r_end; ++r) {
const std::size_t c_start = (r0 == c0) ? (r + 1) : c0;
for (std::size_t c = c_start; c < c_end; ++c) {
std::swap(mat[r * n + c], mat[c * n + r]);
}
}
}
}
// Step 2: Reverse each row sequentially
for (std::size_t r = 0; r < n; ++r) {
auto row_begin = mat.begin() + static_cast<std::ptrdiff_t>(r * n);
std::reverse(row_begin, row_begin + static_cast<std::ptrdiff_t>(n));
}
}
When writing an in-place square matrix transpose, iterating for (r = 0; r < N; ++r) for (c = 0; c < N; ++c) swap(M[r][c], M[c][r]) swaps every off-diagonal pair twice—leaving the matrix completely unchanged! Always restrict the inner loop strictly to the upper triangle for (c = r + 1; c < N; ++c) (or lower triangle c < r). Furthermore, never forget k %= n at the start of array rotation—if $k \ge n$, computing a.begin() + k dereferences out-of-bounds memory.