top

Search

Software Key Tutorial

.

UpGrad

Software Key Tutorial

Matrix Chain Multiplication

Introduction

Matrix chain multiplication is a fundamental topic in computer science that plays a key role in several disciplines, including optimization, numerical analysis, and graph theory. The objective is to efficiently multiply a chain of matrices while reducing the amount of scalar multiplications necessary. This tutorial will introduce you to the dynamic programming solution, which considerably decreases the time complexity for bigger matrices.

Overview

Finding the most effective technique to multiply the number of matrices together is a computer science problem known as matrix chain multiplication. The objective is to reduce the overall number of scalar multiplications needed.

What Is the Problem Statement for the Matrix Chain Multiplication Problem?

The problem statement for the Matrix Chain Multiplication problem is to determine the most efficient technique to multiply a given collection of matrices. The purpose is to determine the procedure in which the matrices should be multiplied to decrease the number of math steps required to compute the final result.

Here is an example to show the problem:

Suppose we have three matrices: A with dimensions 10×20, B with dimensions 20×30, and C with dimensions 30×40. There are various methods to parenthesize the product of these matrices. However, the number of operations necessary might vary depending on the multiplication sequence.

Option 1: (AB)C

The product of A and B is a matrix with a size of 10×30.

The product of the resultant matrix and C is a matrix with a size of 10×40.

Total amount of operations: (10×20×30) + (10×30×40) = 6000 + 12000 = 18000 operations.

Option 2: A(BC)

The product of B and C is a matrix with a size of 20×40.

The product of A and the resultant matrix is a matrix of size 10×40.

Total amount of operations: (20×30×40) + (10×20×40) = 24000 + 8000 = 32000 operations.

In this case, Option 1 involves fewer operations than Option 2. Hence it is the most efficient option to multiply the matrices.

The Matrix Chain Multiplication issue may be addressed using dynamic programming. By evaluating all conceivable methods to partition the series of matrices into subchains and computing the lowest number of operations for each subchain, we may build up a solution for the complete sequence.

What Is the Recursive Solution to the Matrix Chain Multiplication Problem?

The recursive solution to the Matrix Chain Multiplication issue includes breaking down the problem into subproblems and solving them recursively. Here's an explanation of the recursive solution with an example:

  • Base Case: If the range of matrices is limited (just one matrix), return 0 since no scalar multiplications are required.

  • Recursive Step: For each feasible division point, calculate the number of scalar multiplications necessary and pick the least among them.

Let's explore an example using a succession of matrices: A, B, C, D. The dimensions of the matrices are as follows:

A: 2x3

B: 3x4

C: 4x2

D: 2x5

To obtain the least number of scalar multiplications required to multiply these matrices, we may follow these steps:

  1. Compute the minimal number of scalar multiplications required to multiply the subsequence A to B, B to C, and C to D.

  1. For each feasible division point, determine the number of scalar multiplications necessary. For example, we may compute the number of scalar multiplications required to combine A and B at the partition point between A and B. Then, we recursively compute the minimal number of scalar multiplications needed for the subsequence B to C and C to D.

  1. Choose the smallest among the calculated values for each division point.

Matrix Chain Multiplication example in C++

Code:

#include <iostream>
#include <vector>
#include <climits>

// Function to calculate the minimum number of scalar multiplications
// needed to multiply a sequence of matrices
int matrixChainMultiplication(const std::vector<int>& dimensions) {
    int n = dimensions.size() - 1;
    std::vector<std::vector<int>> dp(n, std::vector<int>(n, 0));

    // Initialize the diagonal elements with 0
    for (int i = 0; i < n; ++i) {
        dp[i][i] = 0;
    }

    // Loop to fill the remaining elements in the dp table
    for (int len = 2; len <= n; ++len) {
        for (int i = 0; i <= n - len + 1; ++i) {
            int j = i + len - 1;
            dp[i][j] = INT_MAX;
            for (int k = i; k < j; ++k) {
                int cost = dp[i][k] + dp[k + 1][j] + dimensions[i] * dimensions[k + 1] * dimensions[j + 1];
                dp[i][j] = std::min(dp[i][j], cost);
            }
        }
    }

    return dp[0][n - 1];
}

int main() {
    std::vector<int> dimensions = {10, 30, 5, 60};

    int minScalarMultiplications = matrixChainMultiplication(dimensions);

    std::cout << "Minimum scalar multiplications: " << minScalarMultiplications << std::endl;

    return 0;
}

In the above example, we're given the dimensions of a sequence of matrices to be multiplied. The matrixChainMultiplication function calculates the minimum number of scalar multiplications needed to multiply the matrices in an optimal order. The dp table is used to store the optimal solutions for subproblems. The dynamic programming approach avoids redundant calculations and optimizes the matrix multiplication order.

Note that the dimensions vector should represent the dimensions of matrices in the sequence. For example, if you have matrices A with dimensions 10x30, B with dimensions 30x5, and C with dimensions 5x60, the dimensions vector would be {10, 30, 5, 60}. The output will provide the minimum number of scalar multiplications needed for optimal matrix chain multiplication.

DP With Memoization

Dynamic Programming (DP) with memoization is a technique for increasing the performance of recursive algorithms by preserving and reusing the results of expensive function calls. This can significantly reduce the method's temporal complexity.

Algorithm for DP Using Memoization

The algorithm for DP utilizing memoization may be stated as follows:

  • Identify the recursive function that has to be optimized.

  • Create a memoization table or array to hold the results of function calls.

  • Before performing a recursive call, verify if the result for the provided arguments already exists in the memoization table. If it does, return the saved result.

  • If the result is not found in the memoization table, compute it recursively and save it in the table.

  • Return the computed result.

Pseudocode

Here is the pseudocode for DP with memoization:

Memo = {}  // Memoization table to store computed results

function dpMemoization(arguments):
    if arguments are in Memo:
        return Memo[arguments]

    if base case:
        result = compute base case value
    else:
        result = recursive call(s) with dpMemoization

    Memo[arguments] = result
    return result

C++ Implementation

Code:

#include <iostream>
#include <unordered_map>

std::unordered_map<int, int> memo; // Memoization table

int fibonacci(int n) {
    if (memo.find(n) != memo.end()) {
        return memo[n];
    }

    int result;
    if (n <= 1) {
        result = n;
    } else {
        result = fibonacci(n - 1) + fibonacci(n - 2);
    }

    memo[n] = result;
    return result;
}

int main() {
    int n = 10;
    std::cout << "Fibonacci(" << n << ") = " << fibonacci(n) << std::endl;
    return 0;
}

In this implementation, we calculate the nth Fibonacci number using dynamic programming with memoization. We use an unordered map memo to store precomputed results for different values of n. Before computing a Fibonacci value, we check if it exists in the memo map. If it does, we return the precomputed value. Otherwise, we calculate it recursively and store it in the memo map.

Java Implementation

Code:

import java.util.HashMap;
import java.util.Map;

public class DPMemoization {
    static Map<Integer, Integer> memo = new HashMap<>(); // Memoization table

    static int fibonacci(int n) {
        if (memo.containsKey(n)) {
            return memo.get(n);
        }

        int result;
        if (n <= 1) {
            result = n;
        } else {
            result = fibonacci(n - 1) + fibonacci(n - 2);
        }

        memo.put(n, result);
        return result;
    }

    public static void main(String[] args) {
        int n = 10;
        System.out.println("Fibonacci(" + n + ") = " + fibonacci(n));
    }
}

This is the same implementation as the C++ version, but in Java. We use a HashMap memo to store precomputed results and avoid redundant calculations.

Bottom-up DP Solution 

In the bottom-up DP approach, you start by solving the smallest subproblems and gradually build up to the desired solution by solving larger subproblems. You avoid the recursion and instead use iterative loops to fill up a table (often an array) that stores solutions to subproblems. This approach is often more intuitive for some people and can be more memory-efficient than memoization.

Algorithm

The algorithm for DP utilizing the bottom-up approach may be stated as follows:

  • Identify the problem's structure and determine the order in which you can compute solutions to subproblems.

  • Create a table (often an array) to store solutions to subproblems.

  • Initialize the base cases (usually for the smallest subproblems).

  • Iterate through the table in the determined order, computing solutions to larger subproblems using solutions from smaller subproblems.

  • The final entry in the table will hold the solution to the original problem.

Pseudocode

Here is the pseudocode for DP with bottom-up approach:

function bottomUpDP(arguments):
    table = createEmptyTable()

    // Initialize base cases
    table[0] = baseCaseValue0
    table[1] = baseCaseValue1

    // Fill the table iteratively
    for i = 2 to n:
        table[i] = computeSolutionFrom(table[i-1], table[i-2], ...)

    return table[n]

C++ Implementation

Code:

#include <iostream>
#include <vector>

int bottomUpFibonacci(int n) {
    if (n <= 1) {
        return n;
    }

    std::vector<int> dp(n + 1);
    dp[0] = 0;
    dp[1] = 1;

    for (int i = 2; i <= n; ++i) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }

    return dp[n];
}

int main() {
    int n = 10;
    std::cout << "Fibonacci(" << n << ") = " << bottomUpFibonacci(n) << std::endl;
    return 0;
}

In this implementation, we calculate the nth Fibonacci number using a bottom-up dynamic programming approach. We use an array dp to store the solutions to subproblems, starting from the base cases (dp[0] = 0 and dp[1] = 1). We then iteratively fill the array using previously computed solutions.

Java Implementation

Code:

public class BottomUpDP {
    static int bottomUpFibonacci(int n) {
        if (n <= 1) {
            return n;
        }

        int[] dp = new int[n + 1];
        dp[0] = 0;
        dp[1] = 1;

        for (int i = 2; i <= n; ++i) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }

        return dp[n];
    }

    public static void main(String[] args) {
        int n = 10;
        System.out.println("Fibonacci(" + n + ") = " + bottomUpFibonacci(n));
    }
}

This is the same implementation as the C++ version, but in Java. We use an array dp to store the solutions to subproblems and fill it using an iterative loop.

Conclusion

Understanding matrix chain multiplication is critical for improving the efficiency of matrix calculations in various applications. We may reduce the total computation cost and improve the efficiency of algorithms that incorporate sequences of matrix multiplications by grasping the underlying ideas and employing dynamic programming techniques. This skill benefits anybody working in fields where matrix operations are essential, such as computer graphics, optimization, and scientific computing.

FAQs

  1. Is there a matrix chain multiplication calculator?

There are various matrix chain multiplication calculators available online. One example is the calculator offered by CalculatorSoup, which allows you to enter the dimensions of the matrices and estimate the best multiplication sequence and the total number of scalar multiplications necessary.

  1. How can I implement matrix chain multiplication in C?

Dynamic programming is one method for performing matrix chain multiplication in C. You may create a 2D array to track how many scalar multiplications are required to multiply each subsequence of matrices. The array may then be filled up, and the best multiplication order is found using nested loops.

  1. What is a matrix chain multiplication example?

Assume we have matrices A, B, C, and D that are 10x20, 20x30, 30x40, and 40x30 in size. We may use the following series of multiplications to multiply these matrices: (A(BC))D. This requires 10,800 scalar multiplications in all.

  1. What is the matrix chain multiplication time complexity?

Dynamic programming has an O(n3) temporal complexity for matrix chain multiplication, where n is the number of matrices in the series. This is due to the approach requiring O(n3) time to compute the best multiplication sequence for every feasible sub-sequence of a matrix.

Leave a Reply

Your email address will not be published. Required fields are marked *