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:
The first elements of the sequence are considered given. Number is called the cardinality of the sequence, and coefficients of the sequence. Typical example: Fibonacci numbers, where , , , ... 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 as erection in 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 ? Only and ... 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:
You can check for yourself and make sure that
, ? ? , :
, " " . .
? , . , . :
,
,
, ,
,
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. .
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)
: A[1..n]
( ). A[i..j]
. i
j
. ,
:
- . , . . , .
- . , .
-
. , : , , .O ( n log n ) -
.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:
k < 0
... Then 0 will outperformk
in the firstmax
.k = 0
... In the firstmax
one, you can simply remove the second argument.k > 0
... Thenmax{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?