Time complexity of a specific function in python - python

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.

Related

Python: calculate (1/1) + (1 + 2) / 2 + ... + (1 + 2 + ... + n) / n [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 10 months ago.
Improve this question
The use of for loops is not allowed. This program is designed to calculate values up to number 'n' which is a value entered by the user. Help on this would be appreciated. A photo of the problem and my code so far is listed below:
import math
counterN = 0 #Numerator
counterD = 0 #Denominator
divNumber = 0 #Numerator/Denominator
nValue = 0 #Variable to add numberator values to
dValue = 0 #Variable to add denominaor values to
#Prompts user for a
print("Please enter a value for calculation: ")
number = int(input())
if (number <= 0):
print("Invalid input given.")
#Adds the numerator up to number entered by user
while (counterN <= number):
counterN = counterN + 1
nValue += counterN
#Denominator calculation
counterD = counterD + 1
dValue += counterD
divNumber = nValue / dValue
divNumber += divNumber
#Outputs value to user
print("The result is" , divNumber)
print(counterN, nValue)
You can use recursion for such problems. I've written a recursive solution for you which I found very intuitive-
global SUM
SUM = 0
def fracSUMCalc(n):
if n == 1:
return 1
SUM = fracSUMCalc(n-1) + (sum(range(1, n+1)) / n)
return SUM
print(fracSUMCalc(n=998))
This prints out 249749.5 which is the answer to the sum of your series till n=998. You can vary n as per your need.
Please note that this solution will work fine on any standard modern day laptop till n=998. For n>998, you'd either have to increase your machine's recursion depth limit or use a different approach to develop a more efficient program.
There is a solution that just uses no loops or recursion at all, just maths
def frac_series_sum(n):
return n + sum(range(n)) * 0.5
print(frac_series_sum(1)) # 1.0
print(frac_series_sum(5)) # 10.0
print(frac_series_sum(100)) # 2575.0
It can be shown (see below) that your sum is equal to
for any natural number n.
Thus, this function calculates the sum you want. It does not involve any recursion, for loops or any kind of iteration (it is O(1)):
def my_calc(n):
"Returns 1/1 + (1 + 2)/2 + ... + (1 + 2 + ... + n)/n"
return 0.25 * n * (n + 3)
This is as efficient as it gets.
If you are not allowed to use the solution above and you need to use a while loop:
def my_inefficient_calc(n):
"Returns 1/1 + (1 + 2)/2 + ... + (1 + 2 + ... + n)/n"
result = 0
i = 1
while (i <= n):
result += (sum(range(1, i + 1))) / i
i += 1
return result
If you are not allowed to use the built-in sum function, you can calculate the sum using a nested while loop:
def even_less_efficient(n):
"Returns 1/1 + (1 + 2)/2 + ... + (1 + 2 + ... + n)/n"
result = 0
i = 1
while (i <= n):
inner_sum = 0
k = 1
while (k <= i):
inner_sum += k
k += 1
result += inner_sum / i
i += 1
return result
i is a loop variable (counter) for the outer loop. It ranges from i = 1 up to n.
k is a loop variable (counter) for the inner loop. It ranges from k = 1 up to i.
The inner loop is responsible for calculating the sum in the numerator for each term. This sum is stored in inner_sum.
Once the sum is calculated for a given i (i.e. once we are done with the inner loop), we divide this sum by i to get one of the terms in the mathematical expression.
The outer loop is responsible for summing all of the terms from i = 1 up to n.
Intuitive Proof
How did I arrive at this?
You want a program that calculates the following sum:
This is an elegant solution that has stuck with me for a long time that I will never forget. I once learned in real analysis of a famous mathematician who at the age of 5 (if I remember correctly) reasoned that the sum of i from i = 1 up to k is:
He did so by writing out the sum twice, but the second in reverse order:
(sum of i from i = 1 to k) = 1 + 2 + ... + (k - 1) + k
(sum of i from i = 1 to k) = k + (k - 1) + ... + 2 + 1
----------------------------------------------------------
(k+1)+ (k+1) +... + (k+1) + (k+1) # k terms
He noticed that each sum has k numbers and the sum of each column is equal to k + 1. So, if you add the two sums, you get
2 * (sum of i from i = 1 to k) = k * (k + 1)
Thus
(sum of i from i = 1 to k) = (k * (k + 1)) / 2
which is the same as the result in the second image above.
Substitution of this result into the left-hand-side of the expression in the first image:
Now apply the result that we derived again to the last expression above:
Thus
or
0.25 * n * (n + 3)
Note: A formal proof of the above result would be a proof by induction to show it is true for any natural number n. This proof by induction part is the easy part. I have omitted such a proof as the above should be obvious to anyone that sees it.
Bro, I just did easier
counterN = 1 #Numerator
result = []
#Prompts user for a
print("Please enter a value for calculation: ")
number = int(input())
if (number <= 0):
print("Invalid input given.")
#Adds the numerator up to number entered by user
while (counterN <= number):
numerator = sum(range(counterN+1))
denominator = counterN
result.append(numerator / denominator)
#solucion
counterN += 1
#Outputs value to user
print("The result is" , sum(result))

Optimising sequence code after Time limit exceeded error

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;

Time and Space complexity of Palindrome Partitioning II

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

How to obtain the result of n(n-1)(n-2) / 6

In my Python book, the question asks to prove the value of x after running the following code:
x = 0
for i in range(n):
for j in range(i+1, n):
for k in range(j+1, n):
x += 1
What I could see is that:
i = 0; j=1; k=2: from 2 to n, x+=1, (n-2) times 1
i = 1; j=2; k=3: from 3 to n, x+=1, (n-3) times 1
...
i=n-3; j=n-2; k=n-1: from n-1 to n, x+=1, just 1
i=n-2; j=n-1; k=n doesn't add 1
So it seems that the x is the sum of series of (n-2) + (n-3) + ... + 1?
I am not sure how to get to the answer of n(n-1)(n-2)/6.
One way to view this is that you have n values and three nested loops which are constructed to have non-overlapping ranges. Thus the number of iterations possible is equal to the number of ways to choose three unique values from n items, or n choose 3 = n!/(3!(n-3)!) = n(n-1)(n-2)/3*2*1 = n(n-1)(n-2)/6.
Just write the for loops as a sigma: S = sum_{i=1}^n sum_{j=i+1}^n sum_{k = j + 1}^n (1).
Try to expand the sum from inner to outer:
S = sum_{i=1}^n sum_{j=i+1}^n (n - j) = sum_{i=1}^n n(n-i) - ((i+1) + (i+2) + ... + n) = sum_{i=1}^n n(n-i) - ( 1+2+...+n - (1+2+...+i)) = sum_{i=1}^n n(n-i) -(n(n+1)/2 - i(i+1)/2) = sum_{i=1}^n n(n+1)/2 + i(i+1)/2 - n*i = n^2(n+1)/2 + sum_{i=1}^n (i^2/2 + i/2 - n*i).
If open this sum and simplify it (it is straightforward) you will get S = n(n-1)(n-2)/6.

Volume of pile of cubes

I'm trying a challenge. The idea is the following:
"Your task is to construct a building which will be a pile of n cubes.
The cube at the bottom will have a volume of n^3, the cube above will
have volume of (n-1)^3 and so on until the top which will have a
volume of 1^3.
You are given the total volume m of the building. Being given m can
you find the number n of cubes you will have to build? If no such n
exists return -1"
I saw that apparently:
2³ + 1 = 9 = 3² and 3 - 1 = 2
3³ + 2³ + 1 = 36 = 6² and 6 - 3 = 3
4³ + 3³ + 2³ + 1 = 100 = 10² and 10 - 6 = 4
5³ + 4³ + 3³ + 2³ + 1 = 225 = 15² and 15 - 10 = 5
6³ + 5³ + 4³ + 3³ + 2³ + 1 = 441 = 21² and 21 - 15 = 6
So if I thought, if I check that a certain number is a square root I can already exclude a few. Then I can start a variable at 1 at take that value (incrementing it) from the square root. The values will eventually match or the former square root will become negative.
So I wrote this code:
def find_nb(m):
x = m**0.5
if (x%1==0):
c = 1
while (x != c and x > 0):
x = x - c
c = c + 1
if (x == c):
return c
else:
return -1
return -1
Shouldn't this work? What am I missing?
I fail a third of the sample set, per example: 10170290665425347857 should be -1 and in my program it gives 79863.
Am I missing something obvious?
You're running up against a floating point precision problem. Namely, we have
In [101]: (10170290665425347857)**0.5
Out[101]: 3189089316.0
In [102]: ((10170290665425347857)**0.5) % 1
Out[102]: 0.0
and so the inner branch is taken, even though it's not actually a square:
In [103]: int((10170290665425347857)**0.5)**2
Out[103]: 10170290665425347856
If you borrow one of the many integer square root options from this question and verify that the sqrt squared gives the original number, you should be okay with your algorithm, at least if I haven't overlooked some corner case.
(Aside: you've already noticed the critical pattern. The numbers 1, 3, 6, 10, 15.. are quite famous and have a formula of their own, which you could use to solve for whether there is such a number that works directly.)
DSM's answer is the one, but to add my two cents to improve the solution...
This expression from Brilliant.org is for summing cube numbers:
sum of k**3 from k=1 to n:
n**2 * (n+1)**2 / 4
This can of course be solved for the total volume in question. This here is one of the four solutions (requiring both n and v to be positive):
from math import sqrt
def n(v):
return 1/2*(sqrt(8*sqrt(v) + 1) - 1)
But this function also returns 79863.0. Now, if we sum all the cube numbers from 1 to n, we get a slightly different result due to the precision error:
v = 10170290665425347857
cubes = n(v) # 79863
x = sum([i**3 for i in range(cubes+1)])
# x = 10170290665425347857, original
x -> 10170290665425347856
I don't know if your answer is correct, but I have another solution to this problem which is waaaay easier
def max_level(remain_volume, currLevel):
if remain_volume < currLevel ** 3:
return -1
if remain_volume == currLevel ** 3:
return currLevel
return max_level(remain_volume - currLevel**3, currLevel + 1)
And you find out the answer with max_level(m, 0). It takes O(n) time and O(1) memory.
I have found a simple solution over this in PHP as per my requirement.
function findNb($m) {
$total = 0;
$n = 0;
while($total < $m) {
$n += 1;
$total += $n ** 3;
}
return $total === $m ? $n : -1;
}
In Python it would be:
def find_nb(m):
total = 0
n = 0
while (total < m):
n = n + 1
total = total + n ** 3
return n if total == m else -1

Categories