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

前端 未结 6 406
半阙折子戏
半阙折子戏 2020-12-30 07:07

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 curren

相关标签:
6条回答
  • 2020-12-30 07:31

    Thanks to will's (first) answer, I had this idea:

    Consider that any positive solution comes only from the 1's along the x and y axes. Each of the recursive calls to f divides each component of the solution by 3, which means we can sum, combinatorially, how many ways each 1 features as a component of the solution, and consider it's "distance" (measured as how many calls of f it is from the target) as a negative power of 3.

    JavaScript code:

    function f(n){
    
      var result = 0;
    
      for (var d=n; d<2*n; d++){
    
        var temp = 0;
    
        for (var NE=0; NE<2*n-d; NE++){
    
          temp += choose(n,NE);
        }
    
        result += choose(d - 1,d - n) * temp / Math.pow(3,d);
      }
    
      return 2 * result;
     }
    
    function choose(n,k){
      if (k == 0 || n == k){
        return 1;
      }
      var product = n;
      for (var i=2; i<=k; i++){
        product *= (n + 1 - i) / i
      }
      return product;
    }
    

    Output:

    for (var i=1; i<8; i++){
      console.log("F(" + i + "," + i + ") = " + f(i));
    }
    
    F(1,1) = 0.6666666666666666
    F(2,2) = 0.8148148148148148
    F(3,3) = 0.8641975308641975
    F(4,4) = 0.8879743941472337
    F(5,5) = 0.9024030889600163
    F(6,6) = 0.9123609205913732
    F(7,7) = 0.9197747256986194
    
    0 讨论(0)
  • 2020-12-30 07:36

    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.

    0 讨论(0)
  • 2020-12-30 07:38

    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.

    0 讨论(0)
  • 2020-12-30 07:40

    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).

    0 讨论(0)
  • 2020-12-30 07:46

    If the question is about how to output all values of the function for 0<=i<N, 0<=j<N, here is a solution in time O(N²) and space O(N). The time behavior is optimal.

    Use a temporary array T of N numbers and set it to all ones, except for the first element.
    
    Then row by row,
    
        use a temporary element TT and set it to 1,
        then column by column, assign simultaneously T[I-1], TT = TT, (TT + T[I-1] + T[I])/3.
    
    0 讨论(0)
  • 2020-12-30 07:47

    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];

    0 讨论(0)
提交回复
热议问题