I have studied matrix chain multiplication, wherein given a sequence of matrices, the goal is to find the most efficient way to multiply matrices. The problem is not actually to perform the multiplications, but merely to decide the sequence of the matrix multiplications involved. That is the reason why I am tasked to make a program that outputs all possible combinations of matrices in a matrix multiplication given n as the number of matrices as an input. For example
n == 1 (A)
n == 2 (AB)
n == 3 (AB)C , A(BC)
n== 4 ((AB)C)D, (A(BC))D, A((BC)D), A(B(CD)), (AB)(CD)
My intial code was below, called by
possible_groupings(4) #4 matrices
def possible_groupings(n):
print("Possible Groupings : ")
total = 0
if(n==1):
print('A')
total = total + 1
elif(n==2):
print('(AB)')
total = total + 1
else:
a = 2
while(a <= n-1):
b = 0
while((b+a) <= (n )):
c = b
d = 0
substr = ''
while (d < c):
substr = substr + chr(65 + d)
d = d + 1
if substr != '':
if len(substr) == 1:
print( substr, end = '')
else:
print('(' + substr + ')', end = '')
print('(', end = '')
while (c < (b +a)):
print(chr(65 + c), end = '');
c = c + 1
print(')', end = '')
e = b+a
substr = ''
while (e < n):
substr = substr + chr(65 + e)
e = e + 1
if substr != '':
if len(substr) == 1:
print( substr, end = '')
else:
print('(' + substr + ')', end = '')
print('')
total = total + 1
b = b + 1
a = a + 1
print('Total : ' + str(total))
The output of the code above when my inout is 4 matrices is:
(AB)(CD)
A(BC)D
(AB)(CD)
(ABC)D
A(BCD)
How can I revise my code. The number of matrices must be in the range 1-26. My head now is aching. Please help.
Matrix Chain Multiplication is one of the optimization problem which is widely used in graph algorithms, signal processing and network industry [1–4]. We can have several ways to multiply the given number of matrices because the matrix multiplication is associative.
In other words, no matter how the product is parenthesized, the result obtained will remain the same. For example, for four matrices A, B, C, and D, there are five possible options: ((AB)C)D = (A(BC))D = (AB)(CD) = A((BC)D) = A(B(CD)).
Explanation: The longest common subsequence problem has both, optimal substructure and overlapping subproblems. Hence, dynamic programming should be used the solve this problem.
We can solve the problem using recursion based on the following facts and observations: Two matrices of size m*n and n*p when multiplied, they generate a matrix of size m*p and the number of multiplications performed are m*n*p. Now, for a given chain of N matrices, the first partition can be done in N-1 ways.
Here is a recursive scheme that works back-to-front.
It is implemented as a generator, part
, that starts with the last multiplication. This last multiplication must be between two factors the left of which is a product over the first j (variable cut
in the code below) matrices ("left block") and the right of which is a product over the remaining matrices ("right block"). j can be anything between 1 and N-1 where N is the number of matrices in the chain.
Therefore, to enumerate all groupings we must loop over j. For each j we must combine each grouping of the left block with each grouping of the right block. To enumerate the groupings of the blocks we use part
itself, i.e. recursion.
def part(names, top=True):
lr = ('', '') if top else '()'
if len(names) <= 1:
yield names
elif len(names)==2:
yield names.join(lr)
else:
for cut in range(1, len(names)):
for left in part(names[:cut], False):
for right in part(names[cut:], False):
yield (left+right).join(lr)
The same logic can be used for the minimizer. This can utilize memoization as provided by functools.lru_cache
:
from functools import lru_cache
from string import ascii_uppercase
@lru_cache(None)
def _min_no_mult(dims):
if len(dims) == 2:
return 0, 'x'
elif len(dims)==3:
return dims[0]*dims[1]*dims[2], 'xx'.join('()')
cuts = ((cut, *_min_no_mult(dims[:cut+1]), *_min_no_mult(dims[cut:]))
for cut in range(1, len(dims)-1))
return min((mnl + mnr + dims[0]*dims[-1]*dims[cut], (nml+nmr).join('()'))
for cut, mnl, nml, mnr, nmr in cuts)
def min_no_mult(dims, names=None):
mn, argmn = _min_no_mult(tuple(dims))
names = iter(ascii_uppercase if names is None else names)
argmn = argmn[1:-1] if len(dims) > 2 else argmn
argmn = ''.join(next(names) if a=='x' else a for a in argmn)
return mn, argmn
Demo:
>>> for i, j in enumerate(part(ascii_uppercase[:6])):
... print(i, j)
...
0 A(B(C(D(EF))))
1 A(B(C((DE)F)))
2 A(B((CD)(EF)))
3 A(B((C(DE))F))
4 A(B(((CD)E)F))
...
38 ((A((BC)D))E)F
39 (((AB)(CD))E)F
40 (((A(BC))D)E)F
41 ((((AB)C)D)E)F
Thanks to memoization, the minimizer can easily handle large numbers of dimensions:
>>> import numpy as np
>>> dims = np.clip(np.arange(-1, 26), 1, None)
>>> np.random.shuffle(dims)
>>> dims
array([ 5, 25, 1, 4, 14, 24, 7, 15, 2, 12, 11, 9, 18, 8, 19, 13, 23,
17, 1, 22, 21, 1, 16, 6, 3, 20, 10])
>>> min_no_mult(dims)
(3383, '(AB)((((((((((CD)E)F)G)H)(I(J(K(L(M(N(O(P(QR))))))))))((ST)U))((VW)X))Y)Z)')
We can query some basic cache statistics:
>>> _min_no_mult.cache_info()
CacheInfo(hits=5450, misses=351, maxsize=None, currsize=351)
This may look unimpressive but keep in mind that each hit cuts an entire subtree.
Indeed, we can once more recycle the recurrence scheme and count the number of bracketings:
@lru_cache(None)
def count(n):
if n <= 2:
return 1
else:
return sum(count(cut) * count(n-cut) for cut in range(1, n))
For 26 matrices there are quite a few ways of parenthesizing them:
>>> print(f"{count(26):,d}")
4,861,946,401,452
It looks like you want to partition the set of characters into all possible subsets, although you do not seem to have taken non-contiguous groupings (such as (AC)(DB)) into account. If so, it is a well-known problem for which well-known solutions exist. See for example How to find all partitions of a set.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With