From e6aae1cf66b7e962b886255703b5802d58f27fd3 Mon Sep 17 00:00:00 2001 From: Pooja Sharma <75516191+Shailaputri@users.noreply.github.com> Date: Mon, 16 Oct 2023 05:02:45 +0530 Subject: [PATCH] Dynamic programming/matrix chain multiplication (#10562) * updating DIRECTORY.md * spell changes * updating DIRECTORY.md * real world applications * updating DIRECTORY.md * Update matrix_chain_multiplication.py Add a non-dp solution with benchmarks. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update matrix_chain_multiplication.py * Update matrix_chain_multiplication.py * Update matrix_chain_multiplication.py --------- Co-authored-by: github-actions <${GITHUB_ACTOR}@users.noreply.github.com> Co-authored-by: Pooja Sharma Co-authored-by: Christian Clauss Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- DIRECTORY.md | 5 +- .../matrix_chain_multiplication.py | 143 ++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 dynamic_programming/matrix_chain_multiplication.py diff --git a/DIRECTORY.md b/DIRECTORY.md index 55781df03..cef1e06b7 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -182,6 +182,7 @@ * [Permutations](data_structures/arrays/permutations.py) * [Prefix Sum](data_structures/arrays/prefix_sum.py) * [Product Sum](data_structures/arrays/product_sum.py) + * [Sparse Table](data_structures/arrays/sparse_table.py) * Binary Tree * [Avl Tree](data_structures/binary_tree/avl_tree.py) * [Basic Binary Tree](data_structures/binary_tree/basic_binary_tree.py) @@ -340,6 +341,7 @@ * [Longest Increasing Subsequence O(Nlogn)](dynamic_programming/longest_increasing_subsequence_o(nlogn).py) * [Longest Palindromic Subsequence](dynamic_programming/longest_palindromic_subsequence.py) * [Longest Sub Array](dynamic_programming/longest_sub_array.py) + * [Matrix Chain Multiplication](dynamic_programming/matrix_chain_multiplication.py) * [Matrix Chain Order](dynamic_programming/matrix_chain_order.py) * [Max Non Adjacent Sum](dynamic_programming/max_non_adjacent_sum.py) * [Max Product Subarray](dynamic_programming/max_product_subarray.py) @@ -370,6 +372,7 @@ * [Builtin Voltage](electronics/builtin_voltage.py) * [Carrier Concentration](electronics/carrier_concentration.py) * [Charging Capacitor](electronics/charging_capacitor.py) + * [Charging Inductor](electronics/charging_inductor.py) * [Circular Convolution](electronics/circular_convolution.py) * [Coulombs Law](electronics/coulombs_law.py) * [Electric Conductivity](electronics/electric_conductivity.py) @@ -524,6 +527,7 @@ * [Simplex](linear_programming/simplex.py) ## Machine Learning + * [Apriori Algorithm](machine_learning/apriori_algorithm.py) * [Astar](machine_learning/astar.py) * [Data Transformations](machine_learning/data_transformations.py) * [Decision Tree](machine_learning/decision_tree.py) @@ -554,7 +558,6 @@ * [Word Frequency Functions](machine_learning/word_frequency_functions.py) * [Xgboost Classifier](machine_learning/xgboost_classifier.py) * [Xgboost Regressor](machine_learning/xgboost_regressor.py) - * [Apriori Algorithm](machine_learning/apriori_algorithm.py) ## Maths * [Abs](maths/abs.py) diff --git a/dynamic_programming/matrix_chain_multiplication.py b/dynamic_programming/matrix_chain_multiplication.py new file mode 100644 index 000000000..084254a61 --- /dev/null +++ b/dynamic_programming/matrix_chain_multiplication.py @@ -0,0 +1,143 @@ +""" +Find the minimum number of multiplications needed to multiply chain of matrices. +Reference: https://www.geeksforgeeks.org/matrix-chain-multiplication-dp-8/ + +The algorithm has interesting real-world applications. Example: +1. Image transformations in Computer Graphics as images are composed of matrix. +2. Solve complex polynomial equations in the field of algebra using least processing + power. +3. Calculate overall impact of macroeconomic decisions as economic equations involve a + number of variables. +4. Self-driving car navigation can be made more accurate as matrix multiplication can + accurately determine position and orientation of obstacles in short time. + +Python doctests can be run with the following command: +python -m doctest -v matrix_chain_multiply.py + +Given a sequence arr[] that represents chain of 2D matrices such that the dimension of +the ith matrix is arr[i-1]*arr[i]. +So suppose arr = [40, 20, 30, 10, 30] means we have 4 matrices of dimensions +40*20, 20*30, 30*10 and 10*30. + +matrix_chain_multiply() returns an integer denoting minimum number of multiplications to +multiply the chain. + +We do not need to perform actual multiplication here. +We only need to decide the order in which to perform the multiplication. + +Hints: +1. Number of multiplications (ie cost) to multiply 2 matrices +of size m*p and p*n is m*p*n. +2. Cost of matrix multiplication is associative ie (M1*M2)*M3 != M1*(M2*M3) +3. Matrix multiplication is not commutative. So, M1*M2 does not mean M2*M1 can be done. +4. To determine the required order, we can try different combinations. +So, this problem has overlapping sub-problems and can be solved using recursion. +We use Dynamic Programming for optimal time complexity. + +Example input: +arr = [40, 20, 30, 10, 30] +output: 26000 +""" +from collections.abc import Iterator +from contextlib import contextmanager +from functools import cache +from sys import maxsize + + +def matrix_chain_multiply(arr: list[int]) -> int: + """ + Find the minimum number of multiplcations required to multiply the chain of matrices + + Args: + arr: The input array of integers. + + Returns: + Minimum number of multiplications needed to multiply the chain + + Examples: + >>> matrix_chain_multiply([1, 2, 3, 4, 3]) + 30 + >>> matrix_chain_multiply([10]) + 0 + >>> matrix_chain_multiply([10, 20]) + 0 + >>> matrix_chain_multiply([19, 2, 19]) + 722 + >>> matrix_chain_multiply(list(range(1, 100))) + 323398 + + # >>> matrix_chain_multiply(list(range(1, 251))) + # 2626798 + """ + if len(arr) < 2: + return 0 + # initialising 2D dp matrix + n = len(arr) + dp = [[maxsize for j in range(n)] for i in range(n)] + # we want minimum cost of multiplication of matrices + # of dimension (i*k) and (k*j). This cost is arr[i-1]*arr[k]*arr[j]. + for i in range(n - 1, 0, -1): + for j in range(i, n): + if i == j: + dp[i][j] = 0 + continue + for k in range(i, j): + dp[i][j] = min( + dp[i][j], dp[i][k] + dp[k + 1][j] + arr[i - 1] * arr[k] * arr[j] + ) + + return dp[1][n - 1] + + +def matrix_chain_order(dims: list[int]) -> int: + """ + Source: https://en.wikipedia.org/wiki/Matrix_chain_multiplication + The dynamic programming solution is faster than cached the recursive solution and + can handle larger inputs. + >>> matrix_chain_order([1, 2, 3, 4, 3]) + 30 + >>> matrix_chain_order([10]) + 0 + >>> matrix_chain_order([10, 20]) + 0 + >>> matrix_chain_order([19, 2, 19]) + 722 + >>> matrix_chain_order(list(range(1, 100))) + 323398 + + # >>> matrix_chain_order(list(range(1, 251))) # Max before RecursionError is raised + # 2626798 + """ + + @cache + def a(i: int, j: int) -> int: + return min( + (a(i, k) + dims[i] * dims[k] * dims[j] + a(k, j) for k in range(i + 1, j)), + default=0, + ) + + return a(0, len(dims) - 1) + + +@contextmanager +def elapsed_time(msg: str) -> Iterator: + # print(f"Starting: {msg}") + from time import perf_counter_ns + + start = perf_counter_ns() + yield + print(f"Finished: {msg} in {(perf_counter_ns() - start) / 10 ** 9} seconds.") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + with elapsed_time("matrix_chain_order"): + print(f"{matrix_chain_order(list(range(1, 251))) = }") + with elapsed_time("matrix_chain_multiply"): + print(f"{matrix_chain_multiply(list(range(1, 251))) = }") + with elapsed_time("matrix_chain_order"): + print(f"{matrix_chain_order(list(range(1, 251))) = }") + with elapsed_time("matrix_chain_multiply"): + print(f"{matrix_chain_multiply(list(range(1, 251))) = }")