Domicules: tilings of the 2n × 2n board (A239273)

A notebook: every code cell below runs when the site is built, and its output is printed under it. On GitHub only the code is shown; the rendered page has the results. It counts the domicule tilings of the 2n \times 2n board, OEIS A239273, and of the h \times w rectangles, OEIS A239264 [2]. It also says how the gallery numbers and samples these tilings, and how every number is checked. The notebooks on cylinders and tori count domicules on boards with glued edges, the same boards are counted on the triangular lattice, and Rectangles by diagonal pieces sorts the tilings by their diagonal pieces.

The problem

A domicule covers two cells that share either a side, as a straight piece, or only a corner, as a diagonal piece. Its tilings are the perfect matchings of the king's graph, where every cell is joined to its eight neighbours. Of the 3 tilings of the 2 \times 2 board, two use straight pieces and the third uses two diagonal pieces that cross. The 8 \times 8 board has 3,263,262,629,905 tilings, and the drawing shows 4 of them, spread over the numbering.

In [1]
const { drawTilings } = require('./site/notes.js');
const { svgs, total } = drawTilings({ rowCount: 8, colCount: 8, pieces: 'domicule', spread: 4 });

figure(svgs, `4 of the ${total} domicule tilings of the 8 × 8 board.`);
Out [1] 0.01 s
#1 #815815657477 #1631631314953 #2447446972429
4 of the 3263262629905 domicule tilings of the 8 × 8 board.

The Modular solver

The cell-by-cell scan only adds counts, so it can run modulo a prime p < 2^{31}: the sum of two residues stays below 2^{32} and fits a Uint32Array cell. domicule-rectangle-modular.js counts modulo several primes in parallel worker threads and rebuilds the exact count with the Chinese remainder theorem. Its Rust and Python implementations, domicule-rectangle-modular.rs and domicule_rectangle_modular.py, do the same. It needs enough primes for their product to exceed the count, and the Kahn-Lovász theorem, proved by Alon and Friedland [1], bounds the number of perfect matchings of a graph with degrees d(v):

\#\text{perfect matchings} \le \prod_{v} \bigl(d(v)!\bigr)^{1/(2d(v))}

Every prime contributes at least 30 bits, so \lceil (\log_2 B + 1)/30 \rceil primes suffice for a bound B. One more prime checks the result: the rebuilt count must agree with the residue modulo that prime, and a mismatch stops the solver.

How many primes the modular solver needs

The bound gives \log_2 of the count from above. Each prime below 2^{31} adds at least 30 bits, and one more prime checks the result.

In [2]
const { calculateLog2Bound } = require('./js/solvers/domicule-rectangle-modular.js');

table(
  ['board', 'log2 of the bound', 'log2 of a(n)', 'primes for the CRT', 'check primes'],
  await Promise.all([2, 4, 6, 8, 10].map(async n => {
    const side = 2 * n;
    const bound = calculateLog2Bound(side, side);
    const actual = (await nb.context.countDomicule({ rowCount: side, colCount: side })).toString(2).length - 1;

    return [`${side}×${side}`, bound.toFixed(1), String(actual), String(Math.ceil((bound + 1) / 30)), '1'];
  })),
);
Out [2] 0.00 s
boardlog2 of the boundlog2 of a(n)primes for the CRTcheck primes
4×411.1811
8×852.74121
12×12125.010151
16×16227.818781
20×20361.3298131

The Chinese remainder theorem at work

The 8 \times 8 board counted modulo the two largest primes below 2^{31}, combined, and checked against a third.

In [3]
const { listPrimes, countModulo, combineResidues } = require('./js/solvers/domicule-rectangle-modular.js');
const primes = listPrimes(3);
const residues = primes.map(p => countModulo(8, 8, p));
const combined = combineResidues(residues.slice(0, 2), primes.slice(0, 2));
const exact = await nb.context.countDomicule({ rowCount: 8, colCount: 8 });

primes.forEach((p, i) => print(`a(4) mod ${p} = ${residues[i]}`));
print(`CRT of the first two: ${combined}`);
print(`check: ${combined} mod ${primes[2]} = ${combined % BigInt(primes[2])}, residue ${residues[2]}`);
print(`exact a(4) = ${exact}`);

if (combined !== exact || combined % BigInt(primes[2]) !== BigInt(residues[2])) {
  throw new Error('the CRT does not reproduce a(4)');
}
Out [3] 0.01 s
a(4) mod 2147483647 = 1234970112
a(4) mod 2147483629 = 1234997454
a(4) mod 2147483587 = 1235061252
CRT of the first two: 3263262629905
check: 3263262629905 mod 2147483587 = 1235061252, residue 1235061252
exact a(4) = 3263262629905

The BigInt solver

A domicule tiling is counted with a cell-by-cell scan, the transfer by cells. A free cell is matched with its right, down-left, down or down-right neighbour, so the farthest cell a state has to remember lies w + 1 places ahead and a state has w + 2 bits. The table is four times wider than the one of the domino scan. domicule-rectangle-bigint.js keeps the counts as BigInt, and so do its Rust and Python implementations, domicule-rectangle-bigint.rs and domicule_rectangle_bigint.py. In every language this solver is the reference of the modular one. The same scan is in oeis/A239264/A239264.gp, where T(h, w) is A239264, and oeis/A239273/A239273.gp, where a(n) is A239273.

The state of the transfer

The number of states per cell for both pieces, and the size of the domicule numbering table on the gallery boards.

In [4]
table(
  ['board', 'states per cell (dominoes)', 'states per cell (domicules)', 'numbering table (domicules)'],
  [8, 12, 14, 16, 18, 20].map(side => [
    `${side}×${side}`,
    (2 ** side).toLocaleString('en-US'),
    (2 ** (side + 2)).toLocaleString('en-US'),
    ((side * side + 1) * 2 ** (side + 2)).toLocaleString('en-US'),
  ]),
);
Out [4] 0.01 s
boardstates per cell (dominoes)states per cell (domicules)numbering table (domicules)
8×82561,02466,560
12×124,09616,3842,375,680
14×1416,38465,53612,910,592
16×1665,536262,14467,371,008
18×18262,1441,048,576340,787,200
20×201,048,5764,194,3041,681,915,904

Numbering

Up to 14 \times 14 the gallery numbers every tiling with a table of completion counts of the cell-by-cell scan, as it numbers the domino tilings. The table has (hw + 1) \cdot 2^{w+2} entries, and drawing the number at random gives an exactly uniform tiling.

Sampling with a Markov chain

Domicule tilings have no lattice structure like the domino rotations, so coupling from the past does not apply. From 16 \times 16 to 20 \times 20 the gallery runs a Markov chain. A step picks a cell a and one of its eight neighbours c; when the partner b of a and the partner d of c are neighbours too, the pairs ab and cd become ac and bd. Each such move arises from four ordered choices, and so does the move back, so the proposal is symmetric and the uniform distribution is stationary. On every board small enough to check (up to 4 \times 5) the moves connect all tilings. A chain run for finitely many steps is still only approximately uniform, and the gallery labels its samples as such. Each sample gets its number from the backward scan of the domino sampling; a domicule state has w + 1 bits there, since no piece reaches further than w + 1 cells ahead and the scan shifts before it stores the state.

Is the Markov chain uniform?

On 4 \times 4 the chain can be compared with the exact answer: 280 tilings, each should appear equally often. A chi-squared statistic above 357.8 (279 degrees of freedom, p = 0.001) would reject uniformity.

In [5]
const { sampleDomiculeTiling } = require('./js/renderer');
const { createRandom } = require('./js/renderer/cftp');
const random = createRandom(2024);
const board = { rowCount: 4, colCount: 4 };
const samples = 280 * 100;
const counts = new Map();

for (let i = 0; i < samples; i++) {
  const key = JSON.stringify(sampleDomiculeTiling(board, random, 200));
  counts.set(key, (counts.get(key) || 0) + 1);
}

const expected = samples / 280;
const chiSquared = [...counts.values()].reduce((sum, observed) => sum + (observed - expected) ** 2 / expected, (280 - counts.size) * expected);
print(`distinct tilings seen: ${counts.size} of 280`);
print(`chi-squared: ${chiSquared.toFixed(1)} (limit 357.8)`);

if (counts.size !== 280 || chiSquared > 357.8) {
  throw new Error('the chain fails the uniformity test');
}
Out [5] 2.67 s
distinct tilings seen: 280 of 280
chi-squared: 286.9 (limit 357.8)

The counts

In this table the BigInt solver counts the boards up to 16 \times 16, the modular one 18 \times 18 and 20 \times 20. The data section of the OEIS entry lists the terms up to a(9); the OEIS A239273 b-file continues to a(12) (24 \times 24), beyond the solvers here, and a(10) is checked against it.

In [6]
const reference = require('./js/solvers/domicule-rectangle-bigint.js');
const known = require('./js/core/cases').testCasesOf('domicule-rectangle-bigint');
const bFile = { 10: '974206002957112698855240779762528002509423239819130603938953917534367085458193363176670427' };

const rows = [];

for (let n = 0; n <= 10; n++) {
  const side = 2 * n;
  const board = { rowCount: side, colCount: side };
  const count = n <= 8 ? reference.calculateTotalTilingCombinations(board) : await nb.context.countDomicule(board);
  const published = known.find(t => t.rowCount === side && t.colCount === side);
  const expected = published?.expectedResult ?? bFile[n] ?? null;
  const status = expected === null ? 'not in OEIS yet' : expected !== String(count) ? 'DIFFERS FROM OEIS' : published ? 'matches OEIS' : 'matches the OEIS b-file';

  if (expected !== null && expected !== String(count)) {
    throw new Error(`a(${n}) differs from OEIS`);
  }

  rows.push([n, `${side}×${side}`, String(count), String(count).length, n <= 8 ? 'BigInt' : 'modular', status]);
}

table(['n', 'board', 'a(n)', 'digits', 'solver', 'status'], rows);
Out [6] 3.53 s
nboarda(n)digitssolverstatus
00×011BigIntmatches OEIS
12×231BigIntmatches OEIS
24×42803BigIntmatches OEIS
36×630375617BigIntmatches OEIS
48×8326326262990513BigIntmatches OEIS
510×1032620719551666338193121BigIntmatches OEIS
612×12301188219808243895733014363056331BigIntmatches OEIS
714×14256501434769106220831940461272375210302828843BigIntmatches OEIS
816×1620144262035931368349424531635588356527553184440638495539257BigIntmatches OEIS
918×18145883433280848954911170824766489452422133075800587405307413854042401825973modularmatches OEIS
1020×2097420600295711269885524077976252800250942323981913060393895391753436708545819336317667042790modularmatches the OEIS b-file

Rectangles: A239264

T(h, w) counts the domicule tilings of an h \times w board. The table is symmetric, and its diagonal of even sides is A239273.

In [7]
const { calculateTotalTilingCombinations } = require('./js/solvers/domicule-rectangle-bigint.js');
const sides = [1, 2, 3, 4, 5, 6, 7, 8];

table(
  ['h \\ w', ...sides.map(String)],
  sides.map(h => [String(h), ...sides.map(w => String(calculateTotalTilingCombinations({ rowCount: h, colCount: w })))]),
);
Out [7] 0.01 s
h \ w12345678
101010101
213511214385171
305043045104945
4111432801563941555553331133
5021015630162409017508475
614345194151624093037561552634731017093992
7085055553055263473057228320561
811714945331133175084751017093992572283205613263262629905

Running time

Average time of the count inside each solver, without the start of its process, from the newest complete benchmark run in benchmarks/results.

In [8]
const { formatTime } = nb.context;

table(
  ['board', 'modular, Rust', 'modular, Python', 'modular, JavaScript', 'BigInt, Rust', 'BigInt, Python', 'BigInt, JavaScript'],
  nb.context.domiculeBenchmark.map(r => [
    `${r.rowCount}×${r.colCount}`,
    ...[r.languages.rs, r.languages.py, r.modular, r.bigintLanguages.rs, r.bigintLanguages.py, r.bigint].map(formatTime),
  ]),
);
print(`Run: ${nb.context.benchmarkRun}`);
Out [8] 0.00 s
boardmodular, Rustmodular, Pythonmodular, JavaScriptBigInt, RustBigInt, PythonBigInt, JavaScript
0×00.08 ms2.10 ms9.91 ms0.00 ms0.00 ms0.04 ms
2×20.09 ms3.25 ms10.07 ms0.00 ms0.01 ms0.08 ms
4×40.08 ms3.52 ms9.84 ms0.01 ms0.03 ms0.17 ms
6×60.09 ms3.71 ms10.84 ms0.06 ms0.21 ms0.88 ms
8×80.16 ms8.29 ms13.65 ms0.39 ms1.71 ms3.04 ms
10×100.51 ms18.00 ms33.25 ms2.44 ms11.86 ms10.84 ms
12×123.62 ms41.06 ms81.19 ms14.96 ms71.57 ms59.92 ms
14×1416.25 ms137.54 ms166.88 ms85.68 ms402.60 ms355.03 ms
16×16121.17 ms295.62 ms658.28 ms465.31 ms2.18 s2.17 s
18×18620.02 ms687.77 ms2.95 s2.73 s11.64 s13.12 s
20×204.30 s7.06 s18.46 s19.50 s62.78 s67.70 s
Run: benchmarks/results/ee071e57836d-node26.11.1-python3.14.5-rust1.99.1

PARI/GP

The same transfer in oeis/A239264/A239264.gp, with a(n) = T(2n, 2n) in oeis/A239273/A239273.gp, checked in CI against A239273 up to 12 \times 12 and the A239264 rectangles of the test data.

In [9]
print(['A239264', 'A239273'].map(a => require('fs').readFileSync(require('path').join(nb.context.root, `oeis/${a}/${a}.gp`), 'utf-8').trim()).join('\n\n'));
Out [9] 0.00 s
\\ Domicule tilings of an h X w board (OEIS A239264) = perfect matchings of the king's graph.
\\ Broken-profile dynamic programming in row-major order: bit i of a state tells whether
\\ the cell i places ahead is covered; a free cell is matched right, down-left, down or down-right.
T(h, w) = {
  my(S = 2^(w+2), v = vector(S), u);
  if (h*w == 0, return(1));
  v[1] = 1;
  for (p = 0, h*w-1,
    my(r = p\w, c = p%w, d = []);
    if (c+1 < w, d = concat(d, 1));
    if (r+1 < h,
      if (c > 0, d = concat(d, w-1));
      d = concat(d, w);
      if (c+1 < w, d = concat(d, w+1)));
    u = vector(S);
    for (s = 0, S-1,
      my(x = v[s+1]);
      if (x,
        if (bitand(s, 1),
          u[(s>>1) + 1] += x,
          for (i = 1, #d, if (!bittest(s, d[i]), my(t = (s + 2^d[i])>>1); u[t+1] += x)))));
    v = u);
  v[1]
};

\\ Domicule tilings of a 2n X 2n board (OEIS A239273), with T(h, w) of A239264.gp read first.
a(n) = T(2*n, 2*n);

Verification

Every count on this page is checked in more than one independent way:

The tests are in harness/, the known values in spec/cases/.

References

  1. N. Alon and S. Friedland. The maximum number of perfect matchings in graphs with a given degree sequence. Electronic Journal of Combinatorics 15:N13, 2008. doi:10.37236/888
  2. OEIS Foundation Inc. The On-Line Encyclopedia of Integer Sequences, entries OEIS A239264 and OEIS A239273. oeis.org