#!/usr/bin/python2 # Oppsieee https://github.com/mkutny/absorbing-markov-chains # Doomsday Fuel # ============= # Making fuel for the LAMBCHOP's reactor core is a tricky process because of # the exotic matter involved. It starts as raw ore, then during processing, # begins randomly changing between forms, eventually reaching a stable form. # There may be multiple stable forms that a sample could ultimately reach, not # all of which are useful as fuel. # Commander Lambda has tasked you to help the scientists increase fuel creation # efficiency by predicting the end state of a given ore sample. You have # carefully studied the different structures that the ore can take and which # transitions it undergoes. It appears that, while random, the probability of # each structure transforming is fixed. That is, each time the ore is in 1 # state, it has the same probabilities of entering the next state (which might # be the same state). You have recorded the observed transitions in a matrix. # The others in the lab have hypothesized more exotic forms that the ore can # become, but you haven't seen all of them. # Write a function solution(m) that takes an array of array of nonnegative ints # representing how many times that state has gone to the next state and return # an array of ints for each terminal state giving the exact probabilities of # each terminal state, represented as the numerator for each state, then the # denominator for all of them at the end and in simplest form. The matrix is at # most 10 by 10. It is guaranteed that no matter which state the ore is in, # there is a path from that state to a terminal state. That is, the processing # will always eventually end in a stable state. The ore starts in state 0. The # denominator will fit within a signed 32-bit integer during the calculation, # as long as the fraction is simplified regularly. # For example, consider the matrix m: # [ # [0,1,0,0,0,1], # s0, the initial state, goes to s1 and s5 with equal probability # [4,0,0,3,2,0], # s1 can become s0, s3, or s4, but with different probabilities # [0,0,0,0,0,0], # s2 is terminal, and unreachable (never observed in practice) # [0,0,0,0,0,0], # s3 is terminal # [0,0,0,0,0,0], # s4 is terminal # [0,0,0,0,0,0], # s5 is terminal # ] # So, we can consider different paths to terminal states, such as: # s0 -> s1 -> s3 # s0 -> s1 -> s0 -> s1 -> s0 -> s1 -> s4 # s0 -> s1 -> s0 -> s5 # Tracing the probabilities of each, we find that # s2 has probability 0 # s3 has probability 3/14 # s4 has probability 1/7 # s5 has probability 9/14 # So, putting that together, and making a common denominator, gives an answer in the form of # [s2.numerator, s3.numerator, s4.numerator, s5.numerator, denominator] which is # [0, 3, 2, 9, 14]. from fractions import Fraction from fractions import gcd def num_of_transients(m): if len(m) == 0: raise Exception("Can't get transient states of empty matrix") for r in range(len(m)): for c in range(len(m[r])): if m[r][c] != 0: break else: return r raise Exception("Not a valid AMC matrix: no absorbing (terminal) states") def decompose(m): t = num_of_transients(m) if t == 0: raise Exception("No transient states. At least initial state is needed.") Q = [] for r in range(t): qRow = [] for c in range(t): qRow.append(m[r][c]) Q.append(qRow) if Q == []: raise Exception("Not a valid AMC matrix: no transient states") R = [] for r in range(t): rRow = [] for c in range(t, len(m[r])): rRow.append(m[r][c]) R.append(rRow) if R == []: raise Exception("Not a valid AMC matrix: missing absorbing states") return Q, R def identity(t): m = [] for i in range(t): r = [] for j in range(t): r.append(int(i == j)) m.append(r) return m def isZero(m): for r in range(len(m)): for c in range(len(m[r])): if m[r][c] != 0: return False return True def swap(m, i, j): n = [] s = len(m) if s != len(m[0]): raise Exception("Cannot swap non-square matrix") if i == j: return m for r in range(s): nRow = [] tmpRow = m[r] if r == i: tmpRow = m[j] if r == j: tmpRow = m[i] for c in range(s): tmpEl = tmpRow[c] if c == i: tmpEl = tmpRow[j] if c == j: tmpEl = tmpRow[i] nRow.append(tmpEl) n.append(nRow) return n def sort(m): size = len(m) zero_row = -1 for r in range(size): sum = 0 for c in range(size): sum += m[r][c] if sum == 0: zero_row = r if sum != 0 and zero_row > -1: n = swap(m, r, zero_row) return sort(n) return m def normalize(m, use_fractions=False ): n = [] for r in range(len(m)): sum = 0 cols = len(m[r]) for c in range(cols): sum += m[r][c] nRow = [] if sum == 0: nRow = m[r] else: for c in range(cols): if use_fractions: nRow.append(Fraction(m[r][c], sum)) else: nRow.append(float(m[r][c])/sum) n.append(nRow) return n def subtract(i, q): if len(i) != len(i[0]) or len(q) != len(q[0]): raise Exception("non-square matrices") if len(i) != len(q) or len(i[0]) != len(q[0]): raise Exception("Cannot subtract matrices of different sizes") s = [] for r in range(len(i)): sRow = [] for c in range(len(i[r])): sRow.append(i[r][c] - q[r][c]) s.append(sRow) return s def multiply(a, b): if a == [] or b == []: raise Exception("Cannot multiply empty matrices") if len(a[0]) != len(b): raise Exception("Cannot multiply matrices of incompatible sizes") m = [] rows = len(a) cols = len(b[0]) iters = len(a[0]) for r in range(rows): mRow = [] for c in range(cols): sum = 0 for i in range(iters): sum += a[r][i]*b[i][c] mRow.append(sum) m.append(mRow) return m def transposeMatrix(m): t = [] for r in range(len(m)): tRow = [] for c in range(len(m[r])): if c == r: tRow.append(m[r][c]) else: tRow.append(m[c][r]) t.append(tRow) return t def getMatrixMinor(m,i,j): return [row[:j] + row[j+1:] for row in (m[:i]+m[i+1:])] def getMatrixDeternminant(m): if len(m) == 2: return m[0][0]*m[1][1]-m[0][1]*m[1][0] d = 0 for c in range(len(m)): d += ((-1)**c)*m[0][c]*getMatrixDeternminant(getMatrixMinor(m,0,c)) return d def getMatrixInverse(m): d = getMatrixDeternminant(m) if d == 0: raise Exception("Cannot get inverse of matrix with zero determinant") if len(m) == 2: return [[m[1][1]/d, -1*m[0][1]/d], [-1*m[1][0]/d, m[0][0]/d]] cofactors = [] for r in range(len(m)): cofactorRow = [] for c in range(len(m)): minor = getMatrixMinor(m,r,c) cofactorRow.append(((-1)**(r+c)) * getMatrixDeternminant(minor)) cofactors.append(cofactorRow) cofactors = transposeMatrix(cofactors) for r in range(len(cofactors)): for c in range(len(cofactors)): cofactors[r][c] = cofactors[r][c]/d return cofactors def find_lcm(a): lcm = a[0] for i in a[1:]: lcm = lcm*i//gcd(lcm, i) return lcm def simplify(b): a = [i.denominator for i in b] lcm = find_lcm(a) c = [] for i in range(len(b)): c.append(lcm * b[i].numerator // b[i].denominator) c.append(lcm) return c def solution(m): m = sort(m) n = normalize(m,use_fractions=True) (q, r) = decompose(n) i = identity(len(q)) s = subtract(i, q) v = getMatrixInverse(s) b = multiply(v, r) return simplify(b[0]) print(solution([[0, 2, 1, 0, 0], [0, 0, 0, 3, 4], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]])) # [7, 6, 8, 21] print(solution([[0, 1, 0, 0, 0, 1], [4, 0, 0, 3, 2, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]])) # [0, 3, 2, 9, 14]