Triominoes
A triomino is a shape consisting of three unit squares joined edge-to-edge. There are exactly two combinatorial types: the I-triomino (three collinear squares) and the L-triomino (three squares for...
Problem Statement
This archive keeps the full statement, math, and original media on the page.
A triomino is a shape consisting of three squares joined via the edges. There are two basic forms:

If all possible orientations are taken into account there are six:

Any $n$ by $m$ grid for which $n \times m$ is divisible by $3$ can be tiled with triominoes.
If we consider tilings that can be obtained by reflection or rotation from another tiling as different there are $41$ ways a $2$ by $9$ grid can be tiled with triominoes:
In how many ways can a $9$ by $12$ grid be tiled in this way by triominoes?
Problem 161: Triominoes
Definitions
Definition 1. A tiling of a region by triominoes is a partition of the unit squares of into triominoes such that every square belongs to exactly one triomino.
Definition 2. The broken profile at cell in a row-major traversal of a grid is the bitmask encoding which cells ahead of in the traversal order are already occupied by previously placed triominoes.
Mathematical Development
Theorem 1 (Existence and finiteness). The number of distinct tilings of the grid by triominoes is a well-defined non-negative integer.
Proof. The grid contains unit squares. Since each triomino covers exactly 3 squares and , the necessary divisibility condition is satisfied. The set of valid tilings is a subset of the finite set of all ways to partition 108 cells into groups of 3, hence is finite.
Lemma 1 (Placement geometry). Each of the 6 triomino placement types, anchored at the topmost-leftmost cell in row-major order, occupies cells with the following offsets from the anchor:
| Type | Offsets | Row span | Column span |
|---|---|---|---|
| L-A | 2 | 2 | |
| L-B | 2 | 2 | |
| L-C | 2 | 2 | |
| L-D | 2 | 2 | |
| I-H | 1 | 3 | |
| I-V | 3 | 1 |
Proof. Enumerate by inspection. Each L-triomino is a block with one corner removed, hence spans 2 rows and 2 columns. The I-triomino is either (horizontal) or (vertical).
Theorem 2 (Broken-profile DP correctness). Processing the grid cell-by-cell in row-major order, the state at each cell is fully characterized by a bitmask of width at most bits. The number of distinct tilings equals the value after processing the final cell, where the bitmask indicates all frontier cells are unoccupied.
Proof. We process cells in the order . At cell , any triomino anchored at occupies cells at offsets listed in Lemma 1. The maximum forward reach of any placement in row-major order is positions. However, we only need to track which cells ahead of the current position are already filled.
Since each triomino offset satisfies in the worst case (I-V type), and the current cell is always bit 0, the profile is a bitmask of at most bits (after the current cell is shifted out at each step). This gives possible states.
At each cell, if bit 0 of the profile is set (cell already filled), we shift right by 1 (skip). Otherwise, we try all 6 placements whose cells are within bounds and unoccupied, marking the corresponding bits. The transition is deterministic given the placement choice, and the total count is accumulated additively.
After processing all cells, counts the tilings where every cell is covered exactly once.
Lemma 2 (State space bound). The number of reachable profiles at any step is at most , but empirically is much smaller due to consistency constraints on partial tilings.
Proof. The upper bound follows from the bitmask width. The reduction occurs because many bitmask patterns (e.g., isolated single empty cells surrounded by filled cells) cannot arise from valid partial placements.
Editorial
Count the number of ways to tile a 9x12 grid with triominoes (both L-shaped and I-shaped) using broken-profile dynamic programming. We else. Finally, and the corresponding bits in mask are 0.
Pseudocode
else
and the corresponding bits in mask are 0
Complexity Analysis
Time. Each of the cells is processed once. At each cell, we iterate over all reachable profiles (at most in theory) and try at most 6 placements. This gives worst-case . In practice the reachable state count is far smaller.
Space. for the hash map at any given step.
Answer
Code
Each problem page includes the exact C++ and Python source files from the local archive.
#include <bits/stdc++.h>
using namespace std;
/*
* Project Euler Problem 161: Triominoes
*
* Count the number of ways to tile a 9x12 grid with triominoes
* (L-shaped and I-shaped) via broken-profile dynamic programming.
*/
static const int ROWS = 9;
static const int COLS = 12;
// 6 triomino placements: offsets (dr, dc) from anchor cell
static const int TRI[6][3][2] = {
{{0, 0}, {0, 1}, {1, 0}}, // L-A
{{0, 0}, {0, 1}, {1, 1}}, // L-B
{{0, 0}, {1, 0}, {1, 1}}, // L-C
{{0, 0}, {1, -1}, {1, 0}}, // L-D
{{0, 0}, {0, 1}, {0, 2}}, // I-horizontal
{{0, 0}, {1, 0}, {2, 0}}, // I-vertical
};
int main() {
unordered_map<int, long long> dp, ndp;
dp[0] = 1;
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
ndp.clear();
for (auto &[mask, ways] : dp) {
if (mask & 1) {
ndp[mask >> 1] += ways;
} else {
for (int t = 0; t < 6; t++) {
bool ok = true;
int nmask = mask;
for (int k = 0; k < 3; k++) {
int nr = r + TRI[t][k][0];
int nc = c + TRI[t][k][1];
if (nr < 0 || nr >= ROWS || nc < 0 || nc >= COLS) {
ok = false;
break;
}
int bit = TRI[t][k][0] * COLS + TRI[t][k][1];
if (bit < 0 || (nmask & (1 << bit))) {
ok = false;
break;
}
nmask |= 1 << bit;
}
if (ok)
ndp[nmask >> 1] += ways;
}
}
}
swap(dp, ndp);
}
}
cout << dp[0] << endl;
return 0;
}
"""
Project Euler Problem 161: Triominoes
Count the number of ways to tile a 9x12 grid with triominoes
(both L-shaped and I-shaped) using broken-profile dynamic programming.
"""
from collections import defaultdict
def solve():
ROWS, COLS = 9, 12
# All 6 triomino placements anchored at (0,0) in row-major order:
# L-A: (0,0),(0,1),(1,0) L-B: (0,0),(0,1),(1,1)
# L-C: (0,0),(1,0),(1,1) L-D: (0,0),(1,-1),(1,0)
# I-H: (0,0),(0,1),(0,2) I-V: (0,0),(1,0),(2,0)
triominoes = [
[(0, 0), (0, 1), (1, 0)],
[(0, 0), (0, 1), (1, 1)],
[(0, 0), (1, 0), (1, 1)],
[(0, 0), (1, -1), (1, 0)],
[(0, 0), (0, 1), (0, 2)],
[(0, 0), (1, 0), (2, 0)],
]
dp = defaultdict(int)
dp[0] = 1
for r in range(ROWS):
for c in range(COLS):
ndp = defaultdict(int)
for mask, ways in dp.items():
if mask & 1:
ndp[mask >> 1] += ways
else:
for tri in triominoes:
ok = True
nmask = mask
for dr, dc in tri:
nr, nc = r + dr, c + dc
if nr < 0 or nr >= ROWS or nc < 0 or nc >= COLS:
ok = False
break
bit = dr * COLS + dc
if bit < 0 or nmask & (1 << bit):
ok = False
break
nmask |= 1 << bit
if ok:
ndp[nmask >> 1] += ways
dp = ndp
print(dp.get(0, 0))
solve()
