DSA DSA In Depth
0%
lesson broken-profile-dpgrid-tilingdynamic-programming

Broken Profile (Plug) DP: Domino Tilings & Grid Connectivity in O(N M 2^M)

7 min read Module 6.3: Advanced DP State Spaces & Algebraic Optimizations Updated Oct 23, 2026
📄 Cheat Sheet
Grid Tiling & Broken-Profile Bitmask Dynamic Programming
CodeChef Grid Tiling & Broken-Profile Bitmask Dynamic Programming Watch on YouTube ↗

Matt Fontaine explains why advancing the DP frontier one cell at a time instead of a full row at a time drops complexity from O(N 4^M) to O(N M 2^M).

0:00 / --:--

1. Full-Row Transitions vs. The Cell-by-Cell Broken Profile Frontier

Consider counting the number of ways to tile an $N \times M$ grid with $1 \times 2$ and $2 \times 1$ dominoes (ensure $M \le N$ by transposing the grid if $M > N$, so $M \le \sqrt{NM}$).

In a naive row-by-row Bitmask DP, transitioning from row $r-1$'s protrusion mask to row $r$'s protrusion mask requires enumerating all horizontal domino placements inside row $r$, costing $O((1 + \sqrt{2})^M)$ or $O(4^M)$ per row.

In Broken Profile DP, we process cells in reading order $(r, c)$. At the moment we stand at cell $(r, c)$, the frontier separating the already-processed prefix from the unprocessed suffix consists of exactly $M$ boundary cells:

  • Cells $(r, 0), (r, 1), \dots, (r, c-1)$ in the current row $r$ (bits $0..c-1$ of mask).
  • Cells $(r-1, c), (r-1, c+1), \dots, (r-1, M-1)$ in the previous row $r-1$ (bits $c..M-1$ of mask).

Core Invariant — Broken Profile Bit Meaning at Cell $(r, c)$

In the $M$-bit profile mask before processing cell $(r, c)$:
- Bit $k$ ($k < c$) is $1$ iff cell $(r, k)$ is already covered (or protrudes downward).
- Bit $c$ is $1$ iff cell $(r-1, c)$ protrudes a vertical domino downward into $(r, c)$ (meaning $(r, c)$ is already covered from above!).
When processing $(r, c)$, we look at bit $c$ of mask:
1. If bit $c$ is $1$ ($(r, c)$ already covered from above): We cannot place a new domino starting at $(r, c)$; we simply clear bit $c$ (mask ^ (1 << c)) so $(r, c)$ does not protrude into $(r+1, c)$.
2. If bit $c$ is $0$ ($(r, c)$ is uncovered): We MUST cover $(r, c)$ in one of two ways:
- Vertical Domino $(r, c) \to (r+1, c)$: Set bit $c$ to $1$ (mask | (1 << c)), protruding into row $r+1$.
- Horizontal Domino $(r, c-1) \to (r, c)$: Alternatively (in the backward-looking convention where bit $0$ means covered and $1$ means uncovered, or looking at bit $c$ and $c+1$), if $c + 1 < M$ and bit $c+1$ is $0$ (cell $(r-1, c+1)$ doesn't protrude into $(r, c+1)$), place a horizontal domino covering $(r, c)$ and $(r, c+1)$ by setting bit $c+1$ to $1$ (mask | (1 << (c + 1))) so $(r, c+1)$ will be seen as covered when the loop steps to $c + 1$!

2. Visual Geometry of the Staircase Frontier

flowchart LR subgraph Grid["Staircase Frontier at Cell (r, c)"] B0["Bits 0 .. c-1:
Row r, Cols 0 .. c-1"] --- Step["Step at Col c:
Cell (r, c) replaces (r-1, c)"] Step --- B1["Bits c .. M-1:
Row r-1, Cols c .. M-1"] end Grid --> Choice{"Is bit c set in mask?
(Already covered?)"} Choice -- "Yes (bit c == 1)" --> Clear["Clear bit c:
next_mask = mask ^ (1 << c)"] Choice -- "No (bit c == 0)" --> Place["1. Vertical: next_mask = mask | (1 << c)
2. Horizontal (if c+1 < M and bit c+1 == 0):
next_mask = mask | (1 << (c+1))"]

Hardware Reality — Always Transpose the Grid So $M = \min(R, C)$

Suppose a grid has $R = 12$ rows and $C = 100$ columns ($R \times C = 1200$). If you orient the profile along $C = 100$, $2^{100}$ is astronomically impossible. Because $R \times C \le 1200$ implies $\min(R, C) \le \lfloor \sqrt{1200} \rfloor = 34$ (and in typical tiling problems $R \le 12, C \le 1000$), transposing the grid so that $M = \min(R, C)$ is the width reduces $2^M$ to $2^{12} = 4096$, solving a $12 \times 1000$ grid in $12{,}000 \times 4096 \approx 4.9 \times 10^7$ simple integer additions (~25 milliseconds)!

3. Complexity Matrix

SOS DP (Sum Over Subsets) Hypercube, Digit DP & Broken Profile Tiling
O(N·2^N) SOS / Broken Profile

Trace dimension-by-dimension SOS DP bit-flips F[mask] += F[mask ^ (1<<i)] without overcounting, Broken Profile staircase wavefront domino tiling, and Digit DP tight-prefix automata.

Step 1 / 7
Custom Input:
Timeline

Invariant & State

SOS DP Invariant: After outer loop dimension i, F[mask] aggregates all submasks that may differ from mask only in bits 0..i (zero duplicate paths!).
🔗 Live Pointer & State Transition Graph — SOS DP (Sum Over Subsets) Hypercube, Digit DP & Broken Profile Tiling Active Pointer Jump/Transition Matched/Optimal
00₂ F[00]… [0] Dim i=0 F[01]… [1] 10₂ F[10]… [2] Dim i=1 F[11]… [3]
[0]F[00]=a₀00₂
[1]F[01]=a₀+a₁ (bit 0)Dim i=0 merged
[2]F[10]=a₂10₂
[3]F[11]=a₂+a₃ (bit 0) → +F[01] (bit 1)Dim i=1 → ∑ all 4
3^N Submask Loop
O(3^N) = 59,049 (N=10)
SOS DP N·2^N
O(N·2^N) = 10,240 (N=10)
Update Rule
if (mask & (1<<i)) F[mask] += F[mask^(1<<i)]
[Step 1] Swept dimensions i = 0..N-1; each hypercube edge is traversed once in O(N · 2^N).
Grid Tiling Method ($N \ge M$) States Kept in Memory Transitions / Step Total Time Complexity Time for $N=100, M=12$
Naive Full-Row Matrix Transition O(2^M) O(2^M) O(N * 4^M) $\approx 1.6 \times 10^9$ ops (TLE)
Broken Profile (Cell-by-Cell Rolling) O(2^M) O(1) ($le 2$ choices) O(N * M * 2^M) $\approx 4.9 \times 10^6$ ops (3 ms)
Matrix Exponentiation on Profile (Huge $N$) O(4^M) Matrix-Matrix Mult O(8^M log N) Best when $M \le 7, N = 10^{18}$
Kasteleyn's Pfaffian / Cosine Product Formula O(1) Closed-Form Cosine Sum O(N * M) Exact float/complex formula (no obstacles)

4. Reference C++20 Implementation

#include <algorithm>
#include <cstdint>
#include <vector>

// Count ways to tile an N x M grid with 1x2 and 2x1 dominoes modulo 10^9 + 7 in O(N * M * 2^min(N, M))
long long count_domino_tilings_broken_profile(int n, int m) {
    if ((1LL * n * m) % 2 != 0) return 0; // Odd number of cells cannot be tiled by dominoes!
    if (m > n) std::swap(n, m);           // Ensure M <= N so 2^M is tiny!

    constexpr long long MOD = 1'000'000'007LL;
    const int num_masks = 1 << m;
    std::vector<long long> dp(num_masks, 0), next_dp(num_masks, 0);
    dp[0] = 1; // Initially, no dominoes protrude into row 0

    for (int r = 0; r < n; ++r) {
        for (int c = 0; c < m; ++c) {
            std::fill(next_dp.begin(), next_dp.end(), 0);
            for (int mask = 0; mask < num_masks; ++mask) {
                long long ways = dp[mask];
                if (!ways) continue;

                if ((mask >> c) & 1) {
                    // Cell (r, c) is already covered by a vertical domino from (r-1, c)
                    // or a horizontal domino from (r, c-1); clear bit c for row r+1
                    int nmask = mask ^ (1 << c);
                    next_dp[nmask] = (next_dp[nmask] + ways) % MOD;
                } else {
                    // Option 1: Place a VERTICAL domino covering (r, c) and (r+1, c)
                    if (r + 1 < n) {
                        int nmask = mask | (1 << c);
                        next_dp[nmask] = (next_dp[nmask] + ways) % MOD;
                    }
                    // Option 2: Place a HORIZONTAL domino covering (r, c) and (r, c+1)
                    // Legal iff c + 1 < m AND cell (r, c+1) is not already covered from (r-1, c+1)
                    if (c + 1 < m && !((mask >> (c + 1)) & 1)) {
                        int nmask = mask | (1 << (c + 1));
                        next_dp[nmask] = (next_dp[nmask] + ways) % MOD;
                    }
                }
            }
            std::swap(dp, next_dp);
        }
    }
    return dp[0]; // After the last row, 0 dominoes may protrude past row n - 1
}

Production Pitfall — Allowing Horizontal Dominoes to Wrap Around Row Boundaries ($c = M - 1 \to c = 0$)

When placing a horizontal domino at cell $(r, c)$ by setting bit $c + 1$ (mask | (1 << (c + 1))), you must guard with if (c + 1 < m)! Without that check, at the rightmost column $c = m - 1$, a horizontal domino would illegally wrap around the edge of the board or shift out of the $M$-bit mask. Similarly, if the grid contains blocked obstacle cells grid[r][c] == '#', an obstacle cell requires that bit $c$ is currently $0$ (no domino protruding into the obstacle) and transitions only to mask (leaving bit $c$ as $0$).

Check yourself Question 1 of 4
Concept Check Core Invariant Check recall

In Broken Profile DP on an $R \times C$ grid where $R = 8$ and $C = 1000$, why does advancing the DP **cell-by-cell** (`(r, c)` in one step) run in $O(R \cdot C \cdot 2^R)$ whereas advancing **column-by-column** via recursive row transitions takes $O(C \cdot 4^R)$ (or $O(C \cdot 3^R)$)?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why does counting Single-Cycle Hamiltonian Tours on a grid (Plug DP) require a base-3 or base-4 profile encoding (tracking parenthesis matching `(` and `)` of path endpoints) instead of a simple 1-bit boolean mask?

Key Takeaways

Broken Profile DP advances the boundary one cell (r, c) at a time instead of one full row at a time, reducing complexity from O(N * 4^M) to O(N * M * 2^M).
Always transpose the grid if M > N so the bitmask width M = min(N, M) is at most sqrt(N * M).
At cell (r, c), bit c == 1 means (r, c) is already covered (so clear bit c); bit c == 0 means we must place either a vertical domino (set bit c) or a horizontal domino (set bit c+1 if bit c+1 == 0).
Blocked obstacle cells are handled seamlessly: at an obstacle (r, c), only masks with bit c == 0 are valid, and they transition directly without placing any domino.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue