Open Source · 7 Languages · 900+ Problems

Project Euler Solutions

Complete solutions in C++, Python & Java — with step-by-step mathematical explanations

All Problems

Problem 980: The Quaternion Group I

View on Project Euler

Project Euler Problem 980 Solution

EulerSolve provides an optimized solution for Project Euler Problem 980, The Quaternion Group I, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary Start from the congruential sequence $a_{n+1}\equiv 8888a_n \pmod{888888883},\qquad a_0=88888888,$ and reduce each term modulo \(3\): \(b_n=a_n\bmod 3\). The residues \(0,1,2\) are read as the letters \(x,y,z\). The first \(50N\) letters are split into \(N\) consecutive blocks of length \(50\), and each block is interpreted as a word in the quaternion group \(Q_8=\{\pm1,\pm x,\pm y,\pm z\}\) with the standard relations \(x^2=y^2=z^2=xyz=-1\). The target quantity \(F(N)\) is the number of ordered pairs of blocks \((A,B)\) whose quaternion values multiply to the identity, equivalently \(B=A^{-1}\) inside \(Q_8\). For \(N=10^6\), a naive \(O(N^2)\) comparison over block pairs is out of the question. The entire solution works because each 50-letter block collapses to one of only eight quaternion states, and those states can be recognized from a tiny parity signature. Mathematical Approach The decisive observation is that a word in the quaternion generators is determined by two mod-2 pieces of information: the parity of the number of \(x\), \(y\), and \(z\) letters, and the parity of the swaps needed to sort the word into canonical order \(x \lt y \lt z\). Reducing a word to canonical order Take one 50-letter block \(W\)....

Detailed mathematical approach

Problem Summary

Start from the congruential sequence

$a_{n+1}\equiv 8888a_n \pmod{888888883},\qquad a_0=88888888,$

and reduce each term modulo \(3\): \(b_n=a_n\bmod 3\). The residues \(0,1,2\) are read as the letters \(x,y,z\). The first \(50N\) letters are split into \(N\) consecutive blocks of length \(50\), and each block is interpreted as a word in the quaternion group \(Q_8=\{\pm1,\pm x,\pm y,\pm z\}\) with the standard relations \(x^2=y^2=z^2=xyz=-1\).

The target quantity \(F(N)\) is the number of ordered pairs of blocks \((A,B)\) whose quaternion values multiply to the identity, equivalently \(B=A^{-1}\) inside \(Q_8\). For \(N=10^6\), a naive \(O(N^2)\) comparison over block pairs is out of the question. The entire solution works because each 50-letter block collapses to one of only eight quaternion states, and those states can be recognized from a tiny parity signature.

Mathematical Approach

The decisive observation is that a word in the quaternion generators is determined by two mod-2 pieces of information: the parity of the number of \(x\), \(y\), and \(z\) letters, and the parity of the swaps needed to sort the word into canonical order \(x \lt y \lt z\).

Reducing a word to canonical order

Take one 50-letter block \(W\). Let \(n_x,n_y,n_z\) be its letter counts, and let

$p_x\equiv n_x \pmod 2,\qquad p_y\equiv n_y \pmod 2,\qquad p_z\equiv n_z \pmod 2.$

Also define \(I(W)\) to be the inversion count of the word modulo \(2\) with respect to the alphabet order \(x \lt y \lt z\). In other words, \(I(W)\) records whether an odd or even number of adjacent swaps is needed to sort the letters.

In the quaternion group the generators anticommute in pairs:

$yx=-xy,\qquad zx=-xz,\qquad zy=-yz.$

Therefore every swap of two out-of-order letters contributes a factor of \(-1\). After sorting the word, we obtain

$W = (-1)^{I(W)} x^{n_x} y^{n_y} z^{n_z}.$

This is why inversion parity is exactly the right sign invariant.

Why a 50-letter block has only four parity masks

Because \(x^2=y^2=z^2=-1\), each exponent can be stripped down to its parity:

$x^{n_x} y^{n_y} z^{n_z}=(-1)^{\lfloor n_x/2\rfloor+\lfloor n_y/2\rfloor+\lfloor n_z/2\rfloor}x^{p_x}y^{p_y}z^{p_z}.$

The block length is fixed at \(50\), so \(n_x+n_y+n_z=50\) is even. Hence \(p_x+p_y+p_z\) must also be even, which means only four parity vectors can occur:

$ (0,0,0),\ (0,1,1),\ (1,0,1),\ (1,1,0). $

If we encode the parity vector as

$m=4p_x+2p_y+p_z,$

then the only reachable masks are \(m\in\{0,3,5,6\}\), although the implementations keep an \(8\times 2\) table for convenience.

Using \(n_x+n_y+n_z=50\), the square contribution can also be written as

$\lfloor n_x/2\rfloor+\lfloor n_y/2\rfloor+\lfloor n_z/2\rfloor=\frac{50-(p_x+p_y+p_z)}{2}.$

So the quaternion value of the whole block is completely determined by the pair \((m,I(W))\).

When two blocks are inverses

The parity mask tells us which coset of the center \(\{\pm1\}\) the block lands in. If \(m=0\), the block evaluates to either \(1\) or \(-1\). If \(m\in\{3,5,6\}\), it evaluates to one of the three order-4 pairs \(\{\pm x\}\), \(\{\pm y\}\), or \(\{\pm z\}\). Distinct masks cannot be inverses of one another, so inverse pairs must come from the same mask.

Now compare the two cases:

When \(m=0\), the value is central, so \(q^{-1}=q\). Two blocks with mask \(0\) are inverse exactly when their sign bits agree, which means their inversion parities agree as well.

When \(m\neq 0\), the value has order \(4\), so \(q^{-1}=-q\). Inside a fixed nonzero mask, taking the inverse flips the sign but stays on the same quaternion axis. Therefore two blocks with the same nonzero mask are inverse exactly when their inversion parities are opposite.

Let \(c_{m,0}\) and \(c_{m,1}\) be the numbers of blocks with mask \(m\) and inversion parity \(0\) or \(1\). Since ordered pairs are counted, one mask contributes

$\Phi(m,c_{m,0},c_{m,1})= \begin{cases} c_{m,0}^2+c_{m,1}^2,& m=0,\\ 2c_{m,0}c_{m,1},& m\neq 0. \end{cases}$

Summing over all masks gives

$F(N)=\sum_{m=0}^{7}\Phi(m,c_{m,0},c_{m,1}).$

Only the four even-parity masks can appear, so the remaining table entries stay zero automatically.

Worked Example

A full block has length \(50\), but a shorter even-length example shows the same algebra. Consider

$A=zxyx,\qquad B=zyxx.$

Both words have count parities \((p_x,p_y,p_z)=(0,1,1)\), so they lie in the same nonzero mask. For \(A\), the inversion count is \(4\), hence

$A = (+1)\,x^2yz = -yz = -x.$

For \(B\), the inversion count is \(5\), hence

$B = (-1)\,x^2yz = +x.$

The parity mask is the same, but the inversion parity flips, so the two words become opposite signs on the same quaternion axis. Consequently

$AB=(-x)(x)=1.$

This is exactly the rule used by the algorithm: same nonzero mask, opposite inversion parity.

How the Code Works

Streaming the generated letters

The C++, Python, and Java implementations never materialize the full \(50N\)-letter stream. They keep the current congruential state \(a\), produce the next residue \(a\bmod 3\), and consume the stream block by block. Within one block they maintain the three count parities \((p_x,p_y,p_z)\) and the inversion parity \(I\).

The update rules come directly from the definition of inversions. Appending an \(x\) creates inversions with every earlier \(y\) and \(z\), so the inversion parity toggles by \(p_y\oplus p_z\). Appending a \(y\) creates inversions with earlier \(z\), so it toggles by \(p_z\). Appending a \(z\) creates no new inversion. After processing the letter, the corresponding count parity bit is toggled.

Collapsing blocks into an \(8\times 2\) table

After 50 letters, the block is represented by its mask \(m=4p_x+2p_y+p_z\) and its inversion parity. The implementation increments one counter in a fixed-size table indexed by those two values. Once all \(N\) blocks have been classified, the final answer is the closed-form aggregation above: for mask \(0\) it adds squares, and for every nonzero reachable mask it adds twice the cross product of the two inversion-parity counts.

Before producing the final result for \(N=10^6\), the implementations also verify the published checkpoints \(F(10)=13\) and \(F(100)=1224\). That confirmation is useful because the whole method depends on getting the sign convention exactly right.

Complexity Analysis

Each block requires exactly 50 generator steps and 50 constant-time state updates, so the total running time is \(O(N)\). There is no quadratic pair scan: the pair counting is reduced to a constant-size summary over at most eight masks and two inversion classes.

Memory usage is \(O(1)\). Apart from the current generator value and a few parity bits, the algorithm stores only the \(8\times 2\) count table. This is the essential gain: millions of blocks are compressed into a handful of counters, and the expensive group comparisons disappear entirely.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=980
  2. Quaternion group: Wikipedia - Quaternion group
  3. Linear congruential generator: Wikipedia - Linear congruential generator
  4. Inversion in discrete mathematics: Wikipedia - Inversion
  5. Group presentation: Wikipedia - Presentation of a group

Mathematical approach · C++ solution · Python solution · Java solution

Previous: Problem 979 · All Project Euler solutions · Next: Problem 981

Need help with a problem? Ask me! 💡
e
✦ Euler GLM 5.2
Hi! I'm Euler. Ask me anything about Project Euler problems, math concepts, or solution approaches! 🧮