Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Algorithm to efficiently determine the [n][n] element in a matrix

This is a question regarding a piece of coursework so would rather you didn't fully answer the question but rather give tips to improve the run time complexity of my current algorithm.

I have been given the following information:

A function g(n) is given by g(n) = f(n,n) where f may be defined recursively by

enter image description here

I have implemented this algorithm recursively with the following code:

public static double f(int i, int j) 
{

    if (i == 0 && j == 0) {
        return 0;
    }
    if (i ==0 || j == 0) {
        return 1;
    }

    return ((f(i-1, j)) + (f(i-1, j-1)) + (f(i, j-1)))/3;
}

This algorithm gives the results I am looking for, but it is extremely inefficient and I am now tasked to improve the run time complexity.

I wrote an algorithm to create an n*n matrix and it then computes every element up to the [n][n] element in which it then returns the [n][n] element, for example f(1,1) would return 0.6 recurring. The [n][n] element is 0.6 recurring because it is the result of (1+0+1)/3.

I have also created a spreadsheet of the result from f(0,0) to f(7,7) which can be seen below:

Results

Now although this is much faster than my recursive algorithm, it has a huge overhead of creating a n*n matrix.

Any suggestions to how I can improve this algorithm will be greatly appreciated!

I can now see that is it possible to make the algorithm O(n) complexity, but is it possible to work out the result without creating a [n][n] 2D array?

I have created a solution in Java that runs in O(n) time and O(n) space and will post the solution after I have handed in my coursework to stop any plagiarism.

like image 693
Thody Avatar asked Feb 27 '15 13:02

Thody


4 Answers

This is another one of those questions where it's better to examine it, before diving in and writing code.

The first thing i'd say you should do is look at a grid of the numbers, and to not represent them as decimals, but fractions instead.

The first thing that should be obvious is that the total number of enter image description here you have is just a measure of the distance from the origin, enter image description here.

If you look at a grid in this way, you can get all of the denominators:

enter image description here

Note that the first row and column are not all 1s - they've been chosen to follow the pattern, and the general formula which works for all of the other squares.

The numerators are a little bit more tricky, but still doable. As with most problems like this, the answer is related to combinations, factorials, and then some more complicated things. Typical entries here include Catalan numbers, Stirling's numbers, Pascal's triangle, and you will nearly always see Hypergeometric functions used.

Unless you do a lot of maths, it's unlikely you're familiar with all of these, and there is a hell of a lot of literature. So I have an easier way to find out the relations you need, which nearly always works. It goes like this:

  1. Write a naive, inefficient algorithm to get the sequence you want.
  2. Copy a reasonably large amount of the numbers into google.
  3. Hope that a result from the Online Encyclopedia of Integer Sequences pops up.

    3.b. If one doesn't, then look at some differences in your sequence, or some other sequence related to your data.

  4. Use the information you find to implement said sequence.

So, following this logic, here are the numerators:

enter image description here

Now, unfortunately, googling those yielded nothing. However, there are a few things you can notice about them, the main being that the first row/column are just powers of 3, and that the second row/column are one less than powers of three. This kind boundary is exactly the same as Pascal's triangle, and a lot of related sequences.

Here is the matrix of differences between the numerators and denominators:

enter image description here

Where we've decided that the f(0,0) element shall just follow the same pattern. These numbers already look much simpler. Also note though - rather interestingly, that these numbers follow the same rules as the initial numbers - except the that the first number is one (and they are offset by a column and a row). T(i,j) = T(i-1,j) + T(i,j-1) + 3*T(i-1,j-1):

                     1 
                  1     1
               1     5     1
            1     9     9     1
         1     13    33    13    1
      1     17    73    73    17    1
   1     21    129   245   192   21    1
1     25    201   593   593   201   25    1

This looks more like the sequences you see a lot in combinatorics.

If you google numbers from this matrix, you do get a hit.

And then if you cut off the link to the raw data, you get sequence A081578, which is described as a "Pascal-(1,3,1) array", which exactly makes sense - if you rotate the matrix, so that the 0,0 element is at the top, and the elements form a triangle, then you take 1* the left element, 3* the above element, and 1* the right element.

The question now is implementing the formulae used to generate the numbers.

Unfortunately, this is often easier said than done. For example, the formula given on the page:

T(n,k)=sum{j=0..n, C(k,j-k)*C(n+k-j,k)*3^(j-k)}

is wrong, and it takes a fair bit of reading the paper (linked on the page) to work out the correct formula. The sections you want are proposition 26, corollary 28. The sequence is mentioned in Table 2 after proposition 13. Note that r=4

The correct formula is given in proposition 26, but there is also a typo there :/. The k=0 in the sum should be a j=0:

enter image description here

Where T is the triangular matrix containing the coefficients.

The OEIS page does give a couple of implementations to calculate the numbers, but neither of them are in java, and neither of them can be easily transcribed to java:

There is a mathematica example:

Table[ Hypergeometric2F1[-k, k-n, 1, 4], {n, 0, 10}, {k, 0, n}] // Flatten 

which, as always, is ridiculously succinct. And there is also a Haskell version, which is equally terse:

a081578 n k = a081578_tabl !! n !! k
a081578_row n = a081578_tabl !! n
a081578_tabl = map fst $ iterate
   (\(us, vs) -> (vs, zipWith (+) (map (* 3) ([0] ++ us ++ [0])) $
                      zipWith (+) ([0] ++ vs) (vs ++ [0]))) ([1], [1, 1])

I know you're doing this in java, but i could not be bothered to transcribe my answer to java (sorry). Here's a python implementation:

from __future__ import division
import math

#
# Helper functions
#

def cache(function):
  cachedResults = {}
  def wrapper(*args):
    if args in cachedResults:
      return cachedResults[args]
    else:
      result = function(*args)
      cachedResults[args] = result
      return result
  return wrapper


@cache
def fact(n):
 return math.factorial(n)

@cache
def binomial(n,k):
  if n < k: return 0
  return fact(n) / ( fact(k) * fact(n-k) )





def numerator(i,j):
  """
  Naive way to calculate numerator
  """
  if i == j == 0:
    return 0
  elif i == 0 or j == 0:
    return 3**(max(i,j)-1)
  else:
    return numerator(i-1,j) + numerator(i,j-1) + 3*numerator(i-1,j-1)

def denominator(i,j):
  return 3**(i+j-1)



def A081578(n,k):
  """
  http://oeis.org/A081578
  """
  total = 0
  for j in range(n-k+1):
    total += binomial(k, j) * binomial(n-k, j) * 4**(j)
  return int(total)

def diff(i,j):
  """
  Difference between the numerator, and the denominator. 
  Answer will then be 1-diff/denom.
  """
  if i == j == 0:
    return 1/3
  elif i==0 or j==0:
    return 0
  else:
    return A081578(j+i-2,i-1)

def answer(i,j):
  return 1 - diff(i,j) / denominator(i,j)




# And a little bit at the end to demonstrate it works.
N, M = 10,10

for i in range(N):
  row = "%10.5f"*M % tuple([numerator(i,j)/denominator(i,j) for j in range(M)])
  print row

print ""
for i in range(N):
  row = "%10.5f"*M % tuple([answer(i,j) for j in range(M)])
  print row

So, for a closed form:

enter image description here

Where the enter image description here are just binomial coefficients.

Here's the result:

enter image description here

One final addition, if you are looking to do this for large numbers, then you're going to need to compute the binomial coefficients a different way, as you'll overflow the integers. Your answers are lal floating point though, and since you're apparently interested in large f(n) = T(n,n) then I guess you could use Stirling's approximation or something.

like image 59
will Avatar answered Nov 16 '22 02:11

will


Well for starters here are some things to keep in mind:

This condition can only occur once, yet you test it every time through every loop.

if (x == 0 && y == 0) {
    matrix[x][y] = 0;
}

You should instead: matrix[0][0] = 0; right before you enter your first loop and set x to 1. Since you know x will never be 0 you can remove the first part of your second condition x == 0 :

for(int x = 1; x <= i; x++)
        {
            for(int y = 0; y <= j; y++)
            {             
                if (y == 0) {
                    matrix[x][y] = 1;
                }
                else
                matrix[x][y] = (matrix[x-1][y] + matrix[x-1][y-1] + matrix[x][y-1])/3;
            }
        }

No point in declaring row and column since you only use it once. double[][] matrix = new double[i+1][j+1];

like image 27
kemicofa ghost Avatar answered Nov 16 '22 01:11

kemicofa ghost


This algorithm has a minimum complexity of Ω(n) because you just need to multiply the values in the first column and row of the matrix with some factors and then add them up. The factors stem from unwinding the recursion n times.

However you therefore need to do the unwinding of the recursion. That itself has a complexity of O(n^2). But by balancing unwinding and evaluation of recursion, you should be able to reduce complexity to O(n^x) where 1 <= x <= 2. This is some kind of similiar to algorithms for matrix-matrix multiplication, where the naive case has a complexity of O(n^3) but Strassens's algorithm is for example O(n^2.807).

Another point is the fact that the original formula uses a factor of 1/3. Since this is not accurately representable by fixed point numbers or ieee 754 floating points, the error increases when evaluating the recursion successively. Therefore unwinding the recursion could give you higher accuracy as a nice side effect.

For example when you unwind the recursion sqr(n) times then you have complexity O((sqr(n))^2+(n/sqr(n))^2). The first part is for unwinding and the second part is for evaluating a new matrix of size n/sqr(n). That new complexity actually can be simplified to O(n).

like image 1
SpaceTrucker Avatar answered Nov 16 '22 02:11

SpaceTrucker


To describe time complexity we usually use a big O notation. It is important to remember that it only describes the growth given the input. O(n) is linear time complexity, but it doesn't say how quickly (or slowly) the time grows when we increase input. For example:

n=3 -> 30 seconds
n=4 -> 40 seconds
n=5 -> 50 seconds

This is O(n), we can clearly see that every increase of n increases the time by 10 seconds.

n=3 -> 60 seconds
n=4 -> 80 seconds
n=5 -> 100 seconds

This is also O(n), even though for every n we need twice that much time, and the raise is 20 seconds for every increase of n, the time complexity grows linearly.

So if you have O(n*n) time complexity and you will half the number of operations you perform, you will get O(0.5*n*n) which is equal to O(n*n) - i.e. your time complexity won't change.

This is theory, in practice the number of operations sometimes makes a difference. Because you have a grid n by n, you need to fill n*n cells, so the best time complexity you can achieve is O(n*n), but there are a few optimizations you can do:

  • Cells on the edges of the grid could be filled in separate loops. Currently in majority of the cases you have two unnecessary conditions for i and j equal to 0.
  • You grid has a line of symmetry, you could utilize it to calculate only half of it and then copy the results onto the other half. For every i and j grid[i][j] = grid[j][i]

On final note, the clarity and readability of the code is much more important than performance - if you can read and understand the code, you can change it, but if the code is so ugly that you cannot understand it, you cannot optimize it. That's why I would do only first optimization (it also increases readability), but wouldn't do the second one - it would make the code much more difficult to understand.

As a rule of thumb, don't optimize the code, unless the performance is really causing problems. As William Wulf said:

More computing sins are committed in the name of efficiency (without necessarily achieving it) than for any other single reason - including blind stupidity.

EDIT:

I think it may be possible to implement this function with O(1) complexity. Although it gives no benefits when you need to fill entire grid, with O(1) time complexity you can instantly get any value without having a grid at all.

Grid

A few observations:

  • denominator is equal to 3 ^ (i + j - 1)
  • if i = 2 or j = 2, numerator is one less than denominator

EDIT 2:

The numerator can be expressed with the following function:

public static int n(int i, int j) {
    if (i == 1 || j == 1) {
        return 1;
    } else {
        return 3 * n(i - 1, j - 1) + n(i - 1, j) + n(i, j - 1);
    }
}

Very similar to original problem, but no division and all numbers are integers.

like image 1
Jaroslaw Pawlak Avatar answered Nov 16 '22 01:11

Jaroslaw Pawlak