Domino And Tromino Tiling In C++

The domino and tromino tiling problem is a fascinating and classic problem in combinatorial mathematics and computer science. It involves determining the number of ways to cover a 2×n board entirely using dominoes and trominoes without any overlaps or gaps. This problem not only provides insights into algorithm design but also serves as a practical example of dynamic programming, a method that efficiently solves problems by breaking them down into simpler subproblems.

Dominoes and Trominoes

Dominoes: A domino is a rectangular tile covering two squares . It can be placed horizontally or vertically on the board.

Trominoes: A tromino is a tile that covers three squares. There are different types of trominoes, but in this problem, we consider two primary forms: a straight tromino (which is effectively treated like three dominoes in a line) and an L-shaped tromino (which covers three squares in an L shape).

Given a 2×n board , the goal is to compute the number of distinct ways to tile the board using a combination of dominoes and trominoes. This problem can be visualized as arranging tiles so that the entire board is covered exactly once by the tiles without leaving any square uncovered or having any tiles overlap.

Approach-1: Dynamic Programming Approach

Dynamic programming (DP) is a powerful technique for solving complex problems by breaking them down into simpler subproblems, solving each subproblem just once, and storing their solutions. This method significantly improves efficiency by avoiding redundant computations. Here's a detailed explanation of how DP can be applied to the domino and tromino tiling problem.

Dynamic programming is based on two key principles:

Optimal Substructure: A problem has an optimal substructure if an optimal solution to the problem contains optimal solutions to its subproblems.

Overlapping Subproblems: A problem has overlapping subproblems if the problem can be broken down into subproblems that are reused several times.

The domino and tromino tiling problem perfectly fits these principles, as tiling a 2×n board can be decomposed into smaller tiling problems, and many subproblems overlap.

Program:

Example

#include <iostream>
#include <vector>

using namespace std;
// Function to count the number of ways to tile a 2xn board using dynamic programming
int countTilingWays(int n) {
    // Handle base cases explicitly
    if (n < 0) return 0;
    if (n == 0) return 1;
    if (n == 1) return 1;
    if (n == 2) return 2;    
    // Create a vector to store the number of ways to tile boards of length 0 to n
    vector<int> dp(n + 1, 0);
    dp[0] = 1;  // Base case: One way to tile an empty board
    dp[1] = 1;  // Base case: One way to tile a 2x1 board
    dp[2] = 2;  // Base case: Two ways to tile a 2x2 board   
    // Fill the dp array using the recursive formula
    for (int i = 3; i <= n; ++i) {
        dp[i] = dp[i-1] + dp[i-2] + 2 * dp[i-3];
        // Display intermediate steps for better understanding
        cout << "dp[" << i << "] = dp[" << i-1 << "] + dp[" << i-2 << "] + 2 * dp[" << i-3 << "]" << endl;
        cout << "       = " << dp[i-1] << " + " << dp[i-2] << " + 2 * " << dp[i-3] << endl;
        cout << "       = " << dp[i] << endl;
    }  
    return dp[n];
}
int main() {
    int n;    
    // Input validation
    do {
        cout << "Enter the length of the board (n must be a non-negative integer): ";
        cin >> n;
        if (n < 0) {
            cout << "Invalid input. Please enter a non-negative integer." << endl;
        }
    } while (n < 0);
    // Count the number of ways to tile the board and display the result
    int result = countTilingWays(n);
    cout << "Number of ways to tile a 2x" << n << " board: " << result << endl;  
    // Display the filled dp array
    cout << "DP array contents:" << endl;
    for (int i = 0; i <= n; ++i) {
        cout << "dp[" << i << "] = " << countTilingWays(i) << endl;
    }    
    return 0;
}

Output:

Output

Enter the length of the board (n must be a non-negative integer): 8
dp[3] = dp[2] + dp[1] + 2 * dp[0]
       = 2 + 1 + 2 * 1
       = 5
dp[4] = dp[3] + dp[2] + 2 * dp[1]
       = 5 + 2 + 2 * 1
       = 9
dp[5] = dp[4] + dp[3] + 2 * dp[2]
       = 9 + 5 + 2 * 2
       = 18
dp[6] = dp[5] + dp[4] + 2 * dp[3]
       = 18 + 9 + 2 * 5
       = 37
dp[7] = dp[6] + dp[5] + 2 * dp[4]
       = 37 + 18 + 2 * 9
       = 73
dp[8] = dp[7] + dp[6] + 2 * dp[5]
       = 73 + 37 + 2 * 18
       = 146
Number of ways to tile a 2x8 board: 146
DP array contents:
dp[0] = 1
dp[1] = 1
dp[2] = 2
dp[3] = dp[3] = dp[2] + dp[1] + 2 * dp[0]
       = 2 + 1 + 2 * 1
       = 5
5
dp[4] = dp[3] = dp[2] + dp[1] + 2 * dp[0]
       = 2 + 1 + 2 * 1
       = 5
dp[4] = dp[3] + dp[2] + 2 * dp[1]
       = 5 + 2 + 2 * 1
       = 9
9
dp[5] = dp[3] = dp[2] + dp[1] + 2 * dp[0]
       = 2 + 1 + 2 * 1
       = 5
dp[4] = dp[3] + dp[2] + 2 * dp[1]
       = 5 + 2 + 2 * 1
       = 9
dp[5] = dp[4] + dp[3] + 2 * dp[2]
       = 9 + 5 + 2 * 2
       = 18
18
dp[6] = dp[3] = dp[2] + dp[1] + 2 * dp[0]
       = 2 + 1 + 2 * 1
       = 5
dp[4] = dp[3] + dp[2] + 2 * dp[1]
       = 5 + 2 + 2 * 1
       = 9
dp[5] = dp[4] + dp[3] + 2 * dp[2]
       = 9 + 5 + 2 * 2
       = 18
dp[6] = dp[5] + dp[4] + 2 * dp[3]
       = 18 + 9 + 2 * 5
       = 37
37
dp[7] = dp[3] = dp[2] + dp[1] + 2 * dp[0]
       = 2 + 1 + 2 * 1
       = 5
dp[4] = dp[3] + dp[2] + 2 * dp[1]
       = 5 + 2 + 2 * 1
       = 9
dp[5] = dp[4] + dp[3] + 2 * dp[2]
       = 9 + 5 + 2 * 2
       = 18
dp[6] = dp[5] + dp[4] + 2 * dp[3]
       = 18 + 9 + 2 * 5
       = 37
dp[7] = dp[6] + dp[5] + 2 * dp[4]
       = 37 + 18 + 2 * 9
       = 73
73
dp[8] = dp[3] = dp[2] + dp[1] + 2 * dp[0]
       = 2 + 1 + 2 * 1
       = 5
dp[4] = dp[3] + dp[2] + 2 * dp[1]
       = 5 + 2 + 2 * 1
       = 9
dp[5] = dp[4] + dp[3] + 2 * dp[2]
       = 9 + 5 + 2 * 2
       = 18
dp[6] = dp[5] + dp[4] + 2 * dp[3]
       = 18 + 9 + 2 * 5
       = 37
dp[7] = dp[6] + dp[5] + 2 * dp[4]
       = 37 + 18 + 2 * 9
       = 73
dp[8] = dp[7] + dp[6] + 2 * dp[5]
       = 73 + 37 + 2 * 18
       = 146
146

Explanation:

The provided C++ code solves the domino and tromino tiling problem using dynamic programming, an effective method for handling such combinatorial problems. Here, the objective is to determine the number of ways to completely tile a 2×n board using dominoes and trominoes without any overlaps or gaps .

  • Input Validation

The program begins with input validation, ensuring that the user enters a non-negative integer for the length of the board, n. This step involves a do-while loop, which repeatedly prompts the user for input until a valid value is provided. This is crucial for ensuring that the algorithm operates within its defined constraints and prevents potential errors that could arise from invalid input.

  • Dynamic Programming Array Initialization

Once a valid n is obtained, the program initializes a vector dp to store the number of ways to tile boards of lengths from 0 to n. This array is central to the dynamic programming approach , as it stores the solutions to subproblems, which are then used to build up the solution to the main problem.

  • Handling Base Cases

The base cases are explicitly handled:

dp[0]=1: There is exactly one way to tile an empty board, which is to do nothing.

dp[1]=1: There is one way to tile a 2×1 board, by placing one vertical domino.

dp[2]=2: There are two ways to tile a 2×2 board: either by placing two vertical dominoes or by placing two horizontal dominoes.

These base cases are crucial as they provide the initial values needed to start the recursive calculations for larger values of n.

  • Recursive Calculation

The heart of the dynamic programming solution lies in the loop that fills the dp array for n≥3. For each value of i from 3 to n, the program calculates dp[i] using the formula: dp[i]=dp[i-1]+dp[i-2]+2×dp[i-3]

This formula accounts for the three possible configurations of the last tiles on the board: A vertical domino covers the last column, which leaves a 2×(i-1) board.

Two horizontal dominoes cover the last two columns, which leaves a 2×(i-2) board. An L-shaped tromino covers the last three columns, which can be placed in two different orientations, leaving a 2×(i-3) board.

The intermediate steps and calculations for each iteration are printed to the console for better understanding. This detailed breakdown helps in visualizing how the values in the dp array are derived and provides transparency into the dynamic programming process.

  • Displaying Results

After the loop completes, the program outputs the final result, which is the number of ways to tile a 2×n board. Additionally, the program prints the contents of the dp array, showing the number of ways to tile boards of all lengths from 0 to n. This can be useful for debugging and verifying that the calculations are correct.

Complexity Analysis:

Time Complexity

The time complexity of the provided dynamic programming solution can be analyzed based on the number of operations performed within the function countTilingWays.

Initialization and Input Validation:

Initializing the dp array and setting the base cases involve a constant amount of work. Input validation involves a loop that runs until valid input is provided, but this is not dependent on an and thus can be considered O(1) in the context of analyzing the main algorithm.

Filling the DP Array:

The main loop runs from 3 to n. In each iteration, a constant amount of work is done to calculate dp[i] using the recursive formula:dp[i]=dp[i-1]+dp[i-2]+2×dp[i-3]

This loop runs n-2 times , which simplifies to O(n) because as n grows, the subtraction of a constant becomes negligible.

Therefore, the overall time complexity of the countTilingWays function is O(n).

Space Complexity

The space complexity of the algorithm is determined by the space required to store the dp array.

DP Array Storage:

The dp array is used to store the number of ways to tile boards of lengths from 0 to n. The size of the dp array is n+1, meaning it requires O(n) space.

Additional Variables:

The algorithm uses a few additional variables (e.g., n, loop counters), but these require only constant space O(1).

Therefore, the overall space complexity of the countTilingWays function is O(n).

Approach 2: Matrix Exponentiation

In this approach, we represent the problem using matrix exponentiation to find the number of ways to tile a 2×n board. This method is particularly useful when n is large, as it reduces the time complexity from O(n) in dynamic programming to O(logn).

Program:

Example

#include <iostream>
#include <vector>
using namespace std;
const int MOD = 1e9 + 7; // Modulo to handle large numbers
// Define a 3x3 matrix structure
struct Matrix {
    vector<vector<long long>> mat;
    int rows, cols;
    // Constructor to initialize matrix of size rows x cols with zeros
    Matrix(int r, int c) : rows(r), cols(c) {
        mat.resize(rows, vector<long long>(cols, 0));
    }
    // Function to perform matrix multiplication
    Matrix operator*(const Matrix& other) {
        Matrix result(rows, other.cols);
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < other.cols; ++j) {
                for (int k = 0; k < cols; ++k) {
                    result.mat[i][j] = (result.mat[i][j] + mat[i][k] * other.mat[k][j]) % MOD;
                }
            }
        }
        return result;
    }
};
// Function to perform matrix exponentiation
Matrix matrixExponentiation(Matrix base, int exp) {
    Matrix result(base.rows, base.cols);
    // Initialize result as identity matrix
    for (int i = 0; i < base.rows; ++i) {
        result.mat[i][i] = 1;
    }
    while (exp > 0) {
        if (exp % 2 == 1) {
            result = result * base;
        }
        base = base * base;
        exp /= 2;
    }
    return result;
}
// Function to calculate the number of ways to tile a 2xn board using matrix exponentiation
long long countTilingWays(int n) {
    // Base cases
    if (n == 0) return 1; // Empty board
    if (n == 1) return 1; // 2x1 board
    if (n == 2) return 2; // 2x2 board
    // Create transformation matrix T
    Matrix T(3, 3);
    T.mat = {{1, 1, 2}, {1, 0, 0}, {0, 1, 0}};
    // Calculate T^(n-2)
    Matrix result = matrixExponentiation(T, n - 2);
    // The number of ways to tile a 2xn board is given by T^(n-2)[0][0] * 2 + T^(n-2)[0][1] * 1 + T^(n-2)[0][2] * 2
    long long ways = (result.mat[0][0] * 2 + result.mat[0][1] * 1 + result.mat[0][2] * 2) % MOD;
    return ways;
}
int main() {
    // Welcome message and instructions for the user
    cout << "Welcome to the Domino and Tromino Tiling Problem Solver!" << endl;
    cout << "This program calculates the number of ways to tile a 2xn board using dominoes and trominoes." << endl;
    // Get valid input from the user
    int n;
    cout << "Enter the length of the board (n): ";
    cin >> n;
    // Calculate the number of ways to tile the board
    long long result = countTilingWays(n);
    // Display the result
    cout << "Number of ways to tile a 2x" << n << " board: " << result << endl;
    return 0;
}

Output:

Output

Welcome to the Domino and Tromino Tiling Problem Solver!
This program calculates the number of ways to tile a 2xn board using dominoes and trominoes.
Enter the length of the board (n): 2
Number of ways to tile a 2x2 board: 2

Explanation:

The provided code uses matrix exponentiation to efficiently solve the domino and tromino tiling problem for a 2×n board. This approach leverages linear algebra and properties of matrix multiplication to compute the solution in logarithmic time , which is particularly useful for large n.

  • Matrix Structure:

The Matrix structure defines a matrix with rows and columns and provides a constructor for initializing a matrix filled with zeros.

The operator* function implements matrix multiplication, which is essential for matrix exponentiation.

  • Matrix Exponentiation:

The matrixExponentiation function raises a matrix to the power of an exponent using exponentiation by squaring . This method reduces the time complexity to O(logn).

It starts with the identity matrix, which serves as the multiplicative identity in matrix operations.

  • Transformation Matrix:

The transformation matrix T captures the recurrence relation: T(n)=T(n-1)+T(n-2)+2×T(n-3)

  • Handling Large Numbers:

To prevent overflow and adhere to typical constraints in combinatorial problems, all calculations are performed modulo 10^9+7.

  • User Interaction:

The main function prompts the user to enter the board's length n, invokes countTilingWays to compute the result, and then displays the number of ways to tile the board.

Complexity Analysis:

Time Complexity

The time complexity of the matrix exponentiation approach can be broken down into two main parts: matrix multiplication and matrix exponentiation.

Matrix Multiplication:

In this approach, we primarily deal with 3x3 matrices.

Multiplying two 3x3 matrices involves calculating the product of each element in the rows of the first matrix with the columns of the second matrix, which requires 3×3×3=27 scalar multiplications and additions.

Since the size of the matrices is constant (3x3), each matrix multiplication operation is performed in constant time, denoted as O(1).

Matrix Exponentiation:

Matrix exponentiation leverages the exponentiation method by squaring. This method efficiently raises a matrix to a power.

Exponentiation by squaring reduces the number of multiplications needed by repeatedly squaring the base matrix and multiplying it at most log n times, where n is the length of the board.

Each squaring or multiplication of the matrix, being an O(1) operation, results in a time complexity for the exponentiation process of O(logn).

Overall Time Complexity:

This algorithm's overall time complexity is O(logn). This efficiency is achieved because the matrix exponentiation function reduces the number of necessary operations to logarithmic time, making it much faster than straightforward iterative or recursive approaches, which typically have linear time complexity.

Space Complexity

The space complexity of the matrix exponentiation approach is notably efficient.

Matrix Storage:

The algorithm uses a few 3x3 matrices. Each matrix requires 9 units of space (since 3×3=9).

Since the matrices are constant in size, the space required to store them is also constant, denoted as O(1).

Function Call Stack:

The matrix exponentiation function is implemented iteratively rather than recursively. As a result, no additional space is required for a call stack, which would be necessary in a recursive approach.

The space used by the iterative function call is constant, O(1).

Result Storage:

The final result of the number of ways to tile the 2×n board is a single scalar value. Storing this result requires O(1) space.

Overall Space Complexity:

The algorithm's space complexity is O(1), indicating that it uses a constant amount of space irrespective of the input size n. This includes the space for storing the matrices and any intermediate results during computation.

Input Required

This code uses input(). Please provide values below: