Amazingly fast algorithms

While studying programming, I come across examples of impossible algorithms. Intuition says that this cannot be, but the computer refutes it by simply running the code. How can such a problem, which requires a minimum of cubic costs in time, be solved in just a square? And that one I will definitely decide for the line. What? Is there a much more efficient and elegant logarithm algorithm? Amazing!







In this article I will present several of these "pattern-breaking" algorithms, showing that intuition can greatly overestimate the time complexity of a problem.







Interesting? Welcome under the cut!







Calculation of the nth element of the recurrent sequence in the logarithm



By "recurrent" I mean a sequence that satisfies the following equation:











an+k+1=i=1kcian+i







The first k elements of the sequence are considered given. Number k is called the cardinality of the sequence, and ci coefficients of the sequence. Typical example: Fibonacci numbers, where a1=0 , a2=1 , c1=1 , c2=1 ... We get the well-known numbers: 0, 1, 1, 2, 3, 5, 8, 13, ... It seems that there is no difficulty in calculating the n-th element per line, but it turns out it can be done for a logarithm!







Idea: what if you imagine a computation an as erection in n degree of any object? You can raise to a power for the logarithm, just what kind of object will it be? In general, what you need to know to calculate an+2 ? Only an and an+1 ... So this object, whatever it is, must contain these two numbers. There must also be some "natural" way to multiply these objects. Doesn't it look like anything? Much like matrices: they are made up of numbers and can be multiplied! But is there such a matrix, multiplying which with itself, will we consistently get the Fibonacci numbers?







It turns out there is! There she is:







$$ display $$ \ begin {equation *} A = \ begin {pmatrix} 1 & 1 \\ 1 & 0 \ end {pmatrix} \ end {equation *} $$ display $$

A=(1110)







You can check for yourself and make sure that an+1=A1,1n . , !







, ? ? , :







(fn+1fnfnfn1)×(1110)=(fn+2fn+1fn+1fn)







, " " . . A . "":











t1=1t2=1t3=1...tn+3=tn+2+tn+1+tn







A :







(110101100)







? , . , . :







(tn+2tn+1tntn+1tntn1tntn1tn2)×(110101100)=(tn+3tn+2tn+1tn+2tn+1tntn+1tntn1)







, T :







(t5t4t3t4t3t2t3t2t1)=(311111110)







tn+2=(T×An1)1,1 . , A n1 , T A . T A .







, k :







(c11000c20100c30010ck0001)







, , 2k1 , "" .







, O(k3logn) : O(k3) , O(logn) . . , , n44 , n208 . \ , . , n118 .







Matrix :







class Matrix:
    def __init__(self, n):
        self.n = n
        self.rows = [[0 for col in range(n)] for row in range(n)]

    def set(self, row, col, value):
        self.rows[row][col] = value

    def get(self, row, col):
        return self.rows[row][col]

    def __str__(self):
        result = ''
        for row in self.rows:
            result += ' '.join([str(col) for col in row])
            result += '\n'
        return result

    def __mul__(self, other):
        result = Matrix(self.n)
        for row in range(self.n):
            for col in range(self.n):
                s = sum([self.get(row, k) * other.get(k, col) for k in range(self.n)])
                result.set(row, col, s)
        return result

    def __len__(self):
        return self.n

    def __pow__(self, k):
        if k == 0:
            result = Matrix(len(self))
            for i in range(len(self)):
                result.set(i, i, 1)
        elif k == 1:
            result = self
        elif k == 2:
            result = self * self
        else:
            rem = k % 3
            prev = self.__pow__((k - rem) // 3)
            result = prev * prev * prev
            if rem:
                result *= self.__pow__(rem)
        return result
      
      





__pow__



: M ** k



, M



Matrix



. , 3. .







T A , Matrix



:







A = Matrix(3)
A.set(0, 0, 1)
A.set(0, 1, 1)
A.set(1, 0, 1)
A.set(1, 2, 1)
A.set(2, 0, 1)
T = Matrix(3)
T.set(0, 0, 3)
T.set(0, 1, 1)
T.set(0, 2, 1)
T.set(1, 0, 1)
T.set(1, 1, 1)
T.set(1, 2, 1)
T.set(2, 0, 1)
T.set(2, 1, 1)
T.set(2, 2, 0)
n = int(sys.argv[1])
if n:
    print(T * A ** (n - 1))
else:
    print(T ** 0)
      
      





n — , . , , , - . , , .









: A[1..n]



( ). A[i..j]



. i



j



. , O(n3) , O(n2) , O(nlogn) O(n) .







:







  1. . , . . , .
  2. . , .
  3. O(nlogn) . , : , , .
  4. O(n) . T[1..n]



    , i



    - , i



    . , T , T .


. , T[i + 1]



, T[i]



? , i



, , . , T[i + 1]



T[i] + A[i + 1]



, A[i + 1]



, 0, A[i + 1] < 0



. :







T[0] = 0,
T[i + 1] = max{T[i] + A[i + 1], A[i + 1], 0} = max{T[i] + A[i + 1], 0}
      
      





Let us prove the last equality. It is clear that T[i] >= 0



for anyone i



. Let k = A[i + 1]



. Consider three cases:







  1. k < 0



    ... Then 0 will outperform k



    in the first max



    .
  2. k = 0



    ... In the first max



    one, you can simply remove the second argument.
  3. k > 0



    ... Then max{T[i] + k, k, 0} = T[i] + k = max{T[i] + k, 0}



    .


Due to the linearity and simplicity of the equation, the algorithm is quite short:







def kadane(ints):
    prev_sum = 0
    answer = -1
    for n in ints:
        prev_sum = max(prev_sum + n, 0)
        if prev_sum >= answer:
            answer = prev_sum
    return answer
      
      





Conclusion



In both tasks, the dynamic programming technique helped us to qualitatively improve performance. This is no coincidence, dynamics often gives asymptotically optimal algorithms thanks to built-in economy: we only count everything once.







What amazing algorithms do you know?








All Articles