3
\$\begingroup\$

I have the two following algorithms. My analysis says that both of them are \$\mathcal O(m^24^n)\$ i.e they are equivalent for big numbers (more than 32 bits). Is this right? Note that m and n are the bits numbers for x and y

def pow1(x, y):
 if y == 0:
 return 1
 temp = x
 while y > 1:
 y -= 1
 temp *= x
 return temp
def pow2(x, y):
 if y == 0:
 return 1
 temp = pow2(x, y//2)
 if y & 1: return temp * temp * x
 return temp * temp

The complexity of the first

y-1 iterations and in each iteration, a subtraction taking \$\mathcal O (\lg (y-i))\$, and a multiplication taking \$\mathcal O (im\cdot m)\$, hence the total work takes

\$T=\mathcal O(\sum\limits_{i=1}^ym^2i)=\mathcal O\left(m^2\frac{y(y+1)}2\right)=\mathcal O\left(m^24^n\right)\$

Of the second

We have \$n=\lg y\$ calls and for each, we have multiplications taking \$\mathcal O (2^im\cdot 2^im)\$, hence the total work takes \$ T=\mathcal O\left(\sum\limits_{i=1}^{n}\left(4^im^2\right)\right)=\mathcal O\left({4^n}{m^2}\right)\tag{for large $n$}\$

pacmaninbw
26.2k13 gold badges47 silver badges113 bronze badges
asked Sep 11, 2020 at 3:44
\$\endgroup\$
2
  • \$\begingroup\$ Big-O classification describes the change in behavior for ever larger numbers, absolute speed. The second one is typically much faster, but that has nothing to do with their big-O formula. \$\endgroup\$ Commented Sep 11, 2020 at 3:57
  • \$\begingroup\$ @Aganju See update \$\endgroup\$ Commented Sep 11, 2020 at 4:52

2 Answers 2

2
\$\begingroup\$

I think your analysis is correct if multiplication of k-digit and l-digit numbers takes Θ(kl) time, but more efficient multiplication algorithms are known. The most efficient algorithm in wide use (implemented in GMP) is Schönhage-Strassen, which is O(k log k log log k) for numbers of equal length. I don't know the complexity for unequal lengths, but I suspect it's O(l log k log log k) for k < l. Using that algorithm, or any sub-kl algorithm, you should find that the divide-and-conquer approach is faster.

answered Sep 11, 2020 at 6:16
\$\endgroup\$
2
  • \$\begingroup\$ How do you know Schönhage-Strassen is implemented in GMP? \$\endgroup\$ Commented Sep 11, 2020 at 12:17
  • 1
    \$\begingroup\$ @superbrain It's mentioned in the documentation here. Apparently they pad the inputs to the same length, so it's O(l log l log log l). \$\endgroup\$ Commented Sep 11, 2020 at 23:50
1
\$\begingroup\$

As benrg answered, it looks correct if multiplication of k-digit and l-digit numbers takes Θ(kl) time. We can also somewhat verify it experimentally. Let's write a number class that keeps track of the bit operations:

class Int(int):
 def __mul__(self, other):
 global bitops
 bitops += self.bit_length() * other.bit_length()
 return Int(int(self) * other)

And now let's test that a bit, first increasing n by 1:

m n pow1 pow2 pow1 / pow2 pow1 / prev_pow1
10 10 52272170 34951501 1.4955629516454816 None
10 11 209388450 139788522 1.4978944408611745 4.005734791572648
10 12 838148190 559136896 1.4990035463515539 4.002838695257546
10 13 3353781770 2236448811 1.4996014008925151 4.001418615483737
10 14 13417505370 8945532982 1.4999112291015417 4.0007091367784495

Here pow1 is the number of bit operations with pow1 and likewise for pow2. The pow1 / pow2 column shows that pow1 takes about a constant 1.5 times as many bit operations as pow2. And the last column shows that increasing n by 1 quadruples pow1 as predicted by your analysis saying \$O(4^nm^2)\$.

Now let's instead repeatedly double m:

m n pow1 pow2 pow1 / pow2 pow1 / prev_pow1
10 10 52272170 34951501 1.4955629516454816 None
20 10 209101200 139806021 1.495652322441821 4.000239515596923
40 10 836404800 559224041 1.4956524374459073 4.0
80 10 3345619200 2236896081 1.4956524929420716 4.0
160 10 13382476800 8947584161 1.4956525201886839 4.0

We see that pow1 and pow2 again differ only by constant factor 1.5 and that doubling m quadruples the bit operations as expected from \$O(4^nm^2)\$.

Whole code:

class Int(int):
 def __mul__(self, other):
 global bitops
 bitops += self.bit_length() * other.bit_length()
 return Int(int(self) * other)
def pow1(x, y):
 if y == 0:
 return Int(1)
 temp = x
 while y > 1:
 y -= 1
 temp *= x
 return temp
def pow2(x, y):
 if y == 0:
 return Int(1)
 temp = pow2(x, y//2)
 if y & 1: return temp * temp * x
 return temp * temp
m = 10
n = 10
prev_bitops1 = None
for _ in range(5):
 x = Int(2**m - 1)
 y = 2**n - 1
 bitops = 0; pow1(x, y); bitops1 = bitops
 bitops = 0; pow2(x, y); bitops2 = bitops
 print(m, n,
 bitops1, bitops2,
 bitops1 / bitops2,
 prev_bitops1 and bitops1 / prev_bitops1)
 prev_bitops1 = bitops1
 # n += 1
 m *= 2
answered Sep 11, 2020 at 12:48
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.