So I was solving this LeetCode question - https://leetcode.com/problems/palindrome-partitioning-ii/ and have come up with the following most naive brute force recursive solution. Now, I know how to memoize this solution and work my way up to best possible with Dynamic Programming. But in order to find the time/space complexities of further solutions, I want to see how much worse this solution was and I have looked up in multiple places but haven't been able to find a concrete T/S complexity answer.
def minCut(s: str) -> int:
def is_palindrome(start, end):
while start < end:
if not s[start] == s[end]:
return False
start += 1
end -= 1
return True
def dfs_helper(start, end):
if start >= end:
return 0
if is_palindrome(start, end):
return 0
curr_min = inf
# this is the meat of the solution and what is the time complexity of this
for x in range(start, end):
curr_min = min(curr_min, 1 + dfs_helper(start, x) + dfs_helper(x + 1, end))
return curr_min
return dfs_helper(0, len(s) - 1)
Let's take a look at a worst case scenario, i.e. the palindrome check will not allow us to have an early out.
For writing down the recurrence relation, let's say n = end - start, so that n is the length of the sequence to be processed. I'll assume the indexed array accesses are constant time.
is_palindrome will check for palindromity in O(end - start) = O(n) steps.
dfs_helper for a subsequence of length n, calls is_palindrome once and then has 2n recursive calls of lengths 0 through n - 1, each being called two times, plus the usual constant overhead that I will leave out for simplicity.
So, we have
T(0) = 1
T(n) = O(n) + 2 * (sum of T(x) for x=0 to n-1)
# and for simplicity, I will just use
T(n) = n + 2 * (sum of T(x) for x=0 to n-1)
This pattern already has to be at least exponential. We can look at the next few steps:
T(1) = 3 = 1 + 2 * 1 = 1 + 2 * (T(0))
T(2) = 10 = 2 + 2 * 4 = 2 + 2 * (T(0) + T(1))
T(3) = 31 = 3 + 2 * 14 = 3 + 2 * (T(0) + T(1) + T(2))
T(4) = 94 = 4 + 2 * 45 = 4 + 2 * (T(0) + T(1) + T(2) + T(3))
which looks as if this grows approximately as fast as 3^n. We can also show that for n > 2:
T(n) = n + 2 * (sum of T(x) for x=0 to n-1)
T(n) = n + 2 * (T(0) + T(1) + ... + T(n-1))
T(n) = n + 2 * (T(0) + T(1) + ... + T(n-2)) + 2 * T(n-1)
T(n) = 1 + n-1 + 2 * (T(0) + T(1) + ... + T(n-2)) + 2 * T(n-1)
# with
T(n-1) = n-1 + 2 * (sum of T(x) for x=0 to n-2)
T(n-1) = n-1 + 2 * (T(0) + T(1) + ... + T(n-2))
# we can substitute:
T(n) = 1 + T(n-1) + 2 * T(n-1)
T(n) = 1 + 3 * T(n-1)
So, if I'm not mistaken, the asymptotic time complexity should be in θ(3^n), or, allow me to make that joke, even worse than O(no).
For Space complexity: Your function does not explicitly allocate any memory. So, there is only the constant overhead for recursing (assuming python does not optimize this out). The important aspect here is that the two recursion steps will happen one after the other, so that we get the recurrence:
S(0) = 1
S(n) = 1 + S(n-1)
which gives us a space complexity in θ(n).
Related
Hi I'm trying to understand why the time complexity of the next function:
def f(n):
result = 0
jump = 1
cur = 0
while cur < n:
result += cur
if jump*jump < n:
jump *= 2
cur += jump
return result
is O(√n). I understand that the code under the if statement inside the function gets executed until jump >= √n, I also noticed that cur = 1 + 2 + 4 + 8 + 16 + ... but I still can't get the answer.
A little math is needed here.
Suppose that jump^2 is greater than or equal to n after m iterations, and then the jump will not be doubled again. Here we have:
jump = 2^m >= √n
At this time, cur is:
cur = 1 + 2 + 4 + ... + 2^m = 2 ^ (m + 1) - 1
Then, our total number of iterations will not be greater than:
n - (2 ^ (m + 1) - 1)
m + ceil( --------------------- )
2^m
Because we have 2^m >= √n and 2 ^ (m - 1) < √n, so
n - (2 ^ (m + 1) - 1)
m + ceil( --------------------- )
2^m
n - 2√n + 1
< (log √n + 1) + (----------- + 1)
2 √n
n + 1
= log √n + -----
2 √n
Here, it is clear that (n + 1) / √n is O(√n), and the logarithm is also O(√n), so the sum of the two is O(√n).
Because it is about time complexity, we can prove it more loosely. For example, before jump reaches √n, it is O(log_2 √n) complexity, and after that, it is O(√n) complexity. The sum of the two is obviously O(√n) complexity.
FIND THE SEQUENCE SUM
i = 5
j = 9
K = 6
sum all the values from i to j and back to K: 5 + 6 + 7 + 8 + 9 + 8 + 7 + 6
My answer is:
def sequence_sum(i,j,k):
sum = 0
for i in range(i,j+1):
sum += i
for m in range(j-1,k-1,-1):
sum += m
return sum
However, this solution is correct I received a 9/12 out of the test cases and was given an error message that stated that the time limit was exceeded, I was allowed 10 seconds and the message read " Your code did not execute within the time limits. Please optimise your code."
Can I please get some help with this as I don't understand what I am suppose to do here?
Notice the sum from 5 to 9 is the same as:
(5+0) + (5+1) + (5+2) + (5+3) + (5+4)
If we extract 5, we have: 5*(9-5+1) + (sum from 1 to (9-5)).
To get a general formula:
sum(a,b) = (b-a+1) * a + (b-a)*(b-a+1)/2
The second formula is the sum of all natural numbers up to b-a.
so create the following function:
def my_sum(a,b):
return (b-a+1) * a + (b-a)*(b-a+1)/2
thus the sequence_sum(i,j,k):
def sequence_sum(i,j,k):
return my_sum(i,j) + my_sum(k,j-1)
You can simply use range() function returning an iterable + sum function:
sum's time complexity is O(n) and the range function returns a lazy iterable. That is also O(n) operation.
The return statement returns only the reference of the list so it's time complexity is O(1)
return sum(range(i,j))+sum(range(j,k-1,-1))
the sum from 1, to i is i * (i + 1) / 2 let's call it s(i).
and f(i, j) is the sum from i to j.
f(i, j) = i + (i + 1) + ... + j = 1 + 2 + ... + j - (1 + 2 + ... + (i - 1))
so f(i, j) = s(j) - s(i-1)
the sum from j back to k (but we have to eliminate j because already counted), is the same as the sum from k to j-1 which is f(k, j-1) = s(j-1) - s(k-1)
The total answer is: s(j) - s(i-1) + s(j-1) - s(k-1).
The complexity of sfunction is O(1) so this should pass the test cases
Simpler formula for a range sum, including its derivation:
5 + 6 + 7 + 8
= (write all numbers twice and halve the sum)
(5 + 6 + 7 + 8 +
8 + 7 + 6 + 5) / 2
= (sum each vertical pair)
(13 + 13 + 13 + 13) / 2
=
4 * 13 / 2
=
(8 - 5 + 1) * (5 + 8) / 2
So in general for the sum from i to j: (j - i + 1) * (i + j) / 2
TLE is the case here. So you need to optimize your code.
For that, we can have two approaches.
1st approach: Simple looping
Apply a loop from i to j. And check if the current number is greater than or equal to k. If it is then add 2 times of current number. Else keep adding it once.
for n in range(i,j+1):
if n>=k:
sum+= 2*n
else:
sum+= n
2nd approach: Maths
We know that sum of first n integers is n*(n+1)/2
So what you can do is find sum till j, remove the sum till i (i excluded) from it.
Similarly, for k, find the sum till j and remove the sum till k (k excluded) from it.
Add remaining sums of both minus j as it will get considered twice.
i.e.
sum1 = j*(j+1)/2 - i*i(i-1)/2
sum2 = j*(j+1)/2 - k*(k-1)/2
ans = sum1+sum2 - j;
def count(n):
if n == 0 or n == 1:
return 1
else:
sum = 0
left = 0
right = 0
for x in range(1,n+1):
left = count(x-1)
right = count(n-x)
sum += left * right
return sum
I was reading this post and I wondered
if no of different binary search trees from n nodes is
(2n)! / ((n+1)! * n!)
from this post.
Then
what will be the time complexity ? O(n!) ?
what will be the recurrence relation ?
When you call count(n), it's calling each of count(0) to count(n-1) twice.
So I think you can write the recurrence like this:
T(n) = 2 * sum[T(0) upto T(n-1)] + nk where k represents the multiplication and summation part.
Now consider:
T(n+1) = 2 * sum[T(0) upto T(n)] + (n+1)k
= 2 * sum[T(0) upto T(n-1)] + 2T(n) + nk + k
= T(n) + 2T(n) + k
= 3T(n) + O(1)
Solving this, it appears to be of O(3^n) complexity.
I am trying to solve this problem in Python. Noting that only the first kiss requires the alternation, any kiss that is not a part of the chain due to the first kiss can very well have a hug on the 2nd next person, this is the code I have come up with. This is just a simple mathematical calculation, no looping, no iteration, nothing. But still I am getting a timed-out message. Any means to optimize it?
import psyco
psyco.full()
testcase = int(raw_input())
for i in xrange(0,testcase):
n = int(raw_input())
if n%2:
m = n/2;
ans = 2 + 4*(2**m-1);
ans = ans%1000000007;
print ans
else:
m = n/2 - 1
ans = 2 + 2**(n/2) + 4*(2**m-1);
ans = ans%1000000007
print ans
You're computing powers with very large exponents, which is extremely slow if the results are not reduced in process. For example, a naive computation of 10**10000000 % 11 requires creating a 10000000-digit number and taking modulo 11. A better way is modular exponentiation where you reduce modulo 11 after each multiplication and the integer never gets larger.
Python provides built-in modular exponentiation. Use pow(a,b,c) to compute (a**b) % c.
This is under assumption that your algorithm is correct, which I did not verify.
The answer to this is a pretty simple recursion. F(1) = 2 and for F(n) we have two choices:
n = H, then the number of ways to kiss the remaining guests is simply F(n-1)
n = K, then the number of ways to kiss the remaining guests is 2 ** k where k is the number of remaining guests that the princess is not forced to kiss. Since she has to kiss every second remaining guest, k = ceil((n - 1) / 2)
Putting them together, we get F(n) = F(n - 1) + 2 ** ceil((n - 1) / 2)
My attempt, including taking everything mod 1000000007:
from math import ceil
def F(n):
m = 1000000007
a = 2
for i in range(2, n+1):
a = (a + pow(2, int(ceil((i - 1.0) / 2)), m)) % m
return a
EDIT: Updated (much faster and more unreadable! F(1e9) takes about 3 minutes):
def F(n):
m = 1000000007
a = 2
z = 1
for i in xrange(2, n, 2):
z = (z * 2) % m
a = (a + z + z) % m
if (n & 1 == 0):
z = (z * 2) % m
a = (a + z) % m
return a
EDIT 2: After further thought, I realised the above is actually just:
F(n) = (1 + 1) + (2 + 2) + (4 + 4) + ... + (2 ** n/2 + 2 ** n/2)
= 2 * (1 + 2 + 4 + ... + 2 ** n/2)
= 2 * (2 ** (n/2 + 1) - 1)
= 2 ** (n/2 + 2) - 2
But if n is even, the last 2 ** n/2 only occurs once, so we have:
def F(n):
m = 1000000007
z = pow(2, n/2, m)
if (n % 2 == 0):
return (z * 3 - 2) % m
else:
return (z * 4 - 2) % m
Which runs much faster! (Limited by the speed of pow(x, y, z), which I think is O(lg n)?)
And just because, here is the one-liner:
def F(n):
return (pow(2, n/2, 1000000007) * (3 + n % 2) - 2) % 1000000007
Results:
1 => 2
2 => 4
3 => 6
4 => 10
5 => 14
6 => 22
7 => 30
8 => 46
9 => 62
10 => 94
1e6 => 902893650
1e7 => 502879941
1e8 => 251151906
1e9 => 375000001
I came across a question like this
F(1) = 1
F(2n) = F(n)
F(2n+1) = F(n) + F(n+1)
Develop a recursive program to compute F
Some user mentioned using two recursive function calls:
def calc(n):
if n=1 :
return 1
else if(n%2)==0:
return calc(n/2)
else :
return calc(n/2)+calc(n/2+1) **NESTED RECURSION**
Is this a correct logic? Wouldn't the algorithm be exponentially large?
I thought of a simple code like :
def calc(count):
result[1]=1
n=2
for range(1,count):
if n%2=0:
result.append(result[n])
else :
result.append(result[n/2]+result[n/2+1])
return result
Both of these approaches are correct. It is indeed legal to have multiple recursive calls from a function, and the meaning is what you'd think - just do one call, then the next, then the next, etc.
Interestingly, I don't think that the recursive version does make exponentially many calls. It makes at most two recursive calls, but each are on a problem whose size is (approximately) half as large as the original call. Essentially, the recurrence looks like this:
T(1) = 1
T(2) = 1
T(n) <= T(n / 2) + T(n / 2 + 1) + 1
I use "less than or equal to here" to say that in the best case you might just make one call, but in the worst case you make at most two.
I want to prove that this function T(n) <= max{cn + d, a} for some constants c, d, and a. This would prove that T(n) = O(n) and thus makes at most linearly many calls. As our base case, we have that
T(1) = 1
T(2) = 1
so let's set a = 1. For the inductive step, we consider three cases. First, let's consider when floor(n / 2) <= 2 and floor(n / 2 + 1) <= 2:
T(n) <= T(n / 2) + T(n / 2 + 1) + 1
<= 1 + 1 + 1
<= 3
If we assume that cn + d >= 3, when n = 3 or n = 4, then this works out correctly. In particular, this means that 3c + d >= 3 and 4c + d >= 3.
In the next case, let's see what happens if floor(n / 2) <= 2 and floor(n / 2 + 1) >= 2. Then we have that
T(n) <= T(n / 2) + T(n / 2 + 1) + 1
<= 1 + max{c(n / 2 + 1) + d, 1} + 1
<= 2 + max{c(n / 2 + 1) + d, 1}
<= 3 + c(n / 2 + 1) + d
So if we have that 3 + c(n / 2 + 1) + d <= cn + d, this claim still holds. Note that we're only in this case if n = 5, and so this means that we must have that
3 + c(n / 2 + 1) + d <= cn + d
3 + c(n / 2 + 1) <= cn
3 + c(5 / 2 + 1) <= 5c
3 + 5c/2 + c <= 5c
3 + 7c/2 <= 5c
4 <= 3c / 2
8 / 3 <= c
So we must have that c >= 8 / 3.
And finally, the case where neither n / 2 nor n / 2 + 1 are less than three:
T(n) <= T(n / 2) + T(n / 2 + 1) + 1
<= c(n / 2) + d + c(n / 2 + 1) + d + 1
<= cn / 2 + cn / 2 + c + 2d + 1
= cn + c + 2d + 1
This is less than cn + d if
cn + c + 2d + 1 <= cn + d
c + 2d + 1 <= d
c + d + 1 <= 0
This works if d = -c - 1.
From earlier, we know that 3c + d >= 3, which works if 2c - 1 >= 3, or 2c >= 4, so c >= 2. We also have that 4c + d >= 3, which also works if c >= 2. Letting c = 8 / 3, we get that d = -11 / 3, so
T(n) <= max{8n/3 - 11/3, 1}
So T(n) = O(n) and the recursion makes only linearly many calls.
The short version of this is that both the recursive and iterative versions take linear time. Don't fear recursion's exponential-time blowup without being sure it's exponential. :-) Though admittedtly, in this case, I really like the iterative version more. It's clearer, more intuitive, and more immediately O(n).
Recursion isn't bad. Think of recursion as a tool. Some problems can be solved quite nicely with recursion (e.g. Fibonacci numbers), while other problems don't have a recursive bend (a worklist type algorithm).
Why people go awry with recursion is that they think a recursive solution will give them a good reputation. I'd say, solve the problem with the simplest approach possible.
Sometimes that approach is recursion, sometimes it isn't.
Also, a recursive approach can be harder to understand. So, you need to take readability into consideration.
It is easier to estimate the performance of your second algorithm: it is clearly O(n) in space and time, with a small constant. But that does not mean the second algorithm is faster than the first. In fact it isn't!
In Python 2.6.5, your second algorithm is 25x slower than the first at calculating calc(1000), 80x slower at calculating calc(10000), and 1712x slower at calculating calc(65535).
The worst-case behavior for the first algorithm, it seems, is for numbers like 54613 = 11010101010101012. For that value, the first algorithm is only 16x faster than the second.
In the simple recursive algorithm, calc(1000) only involves 50 calls to calc, and even calc(54613) requires only 5166.
This O(log n) algorithm is faster still:
def C(x):
a = 1
b = 0
# Loop invariant: we are going to return a*C(x) + b*C(x + 1).
while x >= 2:
if x % 2 == 0:
a += b
else:
b += a
x //= 2
return a + b
The performance of this algorithm is easy to evaluate, but its correctness isn't. ;-)
You can write a recursive version which is sublinear (O(log n)) in fact.
The key is to notice that you can compute two values for n and n+1 given the two values for [n/2] and [n/2]+1.
So if you think of the two values as one tuple T(n) = (F(n), F(n+1)), then given T([n/2]), you can compute T(n).
So your function will be something like
Tuple F(int n) {
// Base cases:
Tuple result;
if (n is even) {
Tuple t = F(n/2);
result.First = t.First;
result.Second =t.First + t.Second;
return result;
}
if (n is odd) {
Tuple t = F((n-1)/2);
result.First = t.First + t.Second;
result.Second = t.Second;
return result;
}
}
This way you end up making just one recursive call and since you halve the size of input for each call, it is recursive and O(logn).
Exercise: Give an O(log n) time algorithm for Fibonacci numbers using this trick.
Hint: Use the identities: