113

What is pseudopolynomial time? How does it differ from polynomial time? Some algorithms that run in pseudopolynomial time have runtimes like O(nW) (for the 0/1 Knapsack Problem) or O(√n) (for trial division); why doesn't that count as polynomial time?

templatetypedef
  • 328,018
  • 92
  • 813
  • 992
  • Related post - [Complexity of factorial recursive algorithm](https://stackoverflow.com/q/16373065/465053) – RBT Jan 28 '19 at 06:00

2 Answers2

275

To understand the difference between polynomial time and pseudopolynomial time, we need to start off by formalizing what "polynomial time" means.

The common intuition for polynomial time is "time O(nk) for some k." For example, selection sort runs in time O(n2), which is polynomial time, while brute-force solving TSP takes time O(n · n!), which isn't polynomial time.

These runtimes all refer to some variable n that tracks the size of the input. For example, in selection sort, n refers to the number of elements in the array, while in TSP n refers to the number of nodes in the graph. In order to standardize the definition of what "n" actually means in this context, the formal definition of time complexity defines the "size" of a problem as follows:

The size of the input to a problem is the number of bits required to write out that input.

For example, if the input to a sorting algorithm is an array of 32-bit integers, then the size of the input would be 32n, where n is the number of entries in the array. In a graph with n nodes and m edges, the input might be specified as a list of all the nodes followed by a list of all the edges, which would require Ω(n + m) bits.

Given this definition, the formal definition of polynomial time is the following:

An algorithm runs in polynomial time if its runtime is O(xk) for some constant k, where x denotes the number of bits of input given to the algorithm.

When working with algorithms that process graphs, lists, trees, etc., this definition more or less agrees with the conventional definition. For example, suppose you have a sorting algorithm that sorts arrays of 32-bit integers. If you use something like selection sort to do this, the runtime, as a function of the number of input elements in the array, will be O(n2). But how does n, the number of elements in the input array, correspond to the the number of bits of input? As mentioned earlier, the number of bits of input will be x = 32n. Therefore, if we express the runtime of the algorithm in terms of x rather than n, we get that the runtime is O(x2), and so the algorithm runs in polynomial time.

Similarly, suppose that you do depth-first search on a graph, which takes time O(m + n), where m is the number of edges in the graph and n is the number of nodes. How does this relate to the number of bits of input given? Well, if we assume that the input is specified as an adjacency list (a list of all the nodes and edges), then as mentioned earlier the number of input bits will be x = Ω(m + n). Therefore, the runtime will be O(x), so the algorithm runs in polynomial time.

Things break down, however, when we start talking about algorithms that operate on numbers. Let's consider the problem of testing whether a number is prime or not. Given a number n, you can test if n is prime using the following algorithm:

function isPrime(n):
    for i from 2 to n - 1:
        if (n mod i) = 0, return false
    return true

So what's the time complexity of this code? Well, that inner loop runs O(n) times and each time does some amount of work to compute n mod i (as a really conservative upper bound, this can certainly be done in time O(n3)). Therefore, this overall algorithm runs in time O(n4) and possibly a lot faster.

In 2004, three computer scientists published a paper called PRIMES is in P giving a polynomial-time algorithm for testing whether a number is prime. It was considered a landmark result. So what's the big deal? Don't we already have a polynomial-time algorithm for this, namely the one above?

Unfortunately, we don't. Remember, the formal definition of time complexity talks about the complexity of the algorithm as a function of the number of bits of input. Our algorithm runs in time O(n4), but what is that as a function of the number of input bits? Well, writing out the number n takes O(log n) bits. Therefore, if we let x be the number of bits required to write out the input n, the runtime of this algorithm is actually O(24x), which is not a polynomial in x.

This is the heart of the distinction between polynomial time and pseudopolynomial time. On the one hand, our algorithm is O(n4), which looks like a polynomial, but on the other hand, under the formal definition of polynomial time, it's not polynomial-time.

To get an intuition for why the algorithm isn't a polynomial-time algorithm, think about the following. Suppose I want the algorithm to have to do a lot of work. If I write out an input like this:

10001010101011

then it will take some worst-case amount of time, say T, to complete. If I now add a single bit to the end of the number, like this:

100010101010111

The runtime will now (in the worst case) be 2T. I can double the amount of work the algorithm does just by adding one more bit!

An algorithm runs in pseudopolynomial time if the runtime is some polynomial in the numeric value of the input, rather than in the number of bits required to represent it. Our prime testing algorithm is a pseudopolynomial time algorithm, since it runs in time O(n4), but it's not a polynomial-time algorithm because as a function of the number of bits x required to write out the input, the runtime is O(24x). The reason that the "PRIMES is in P" paper was so significant was that its runtime was (roughly) O(log12 n), which as a function of the number of bits is O(x12).

So why does this matter? Well, we have many pseudopolynomial time algorithms for factoring integers. However, these algorithms are, technically speaking, exponential-time algorithms. This is very useful for cryptography: if you want to use RSA encryption, you need to be able to trust that we can't factor numbers easily. By increasing the number of bits in the numbers to a huge value (say, 1024 bits), you can make the amount of time that the pseudopolynomial-time factoring algorithm must take get so large that it would be completely and utterly infeasible to factor the numbers. If, on the other hand, we can find a polynomial-time factoring algorithm, this isn't necessarily the case. Adding in more bits may cause the work to grow by a lot, but the growth will only be polynomial growth, not exponential growth.

That said, in many cases pseudopolynomial time algorithms are perfectly fine because the size of the numbers won't be too large. For example, counting sort has runtime O(n + U), where U is the largest number in the array. This is pseudopolynomial time (because the numeric value of U requires O(log U) bits to write out, so the runtime is exponential in the input size). If we artificially constrain U so that U isn't too large (say, if we let U be 2), then the runtime is O(n), which actually is polynomial time. This is how radix sort works: by processing the numbers one bit at a time, the runtime of each round is O(n), so the overall runtime is O(n log U). This actually is polynomial time, because writing out n numbers to sort uses Ω(n) bits and the value of log U is directly proportional to the number of bits required to write out the maximum value in the array.

Hope this helps!

Prudhvi
  • 2,108
  • 7
  • 31
  • 51
templatetypedef
  • 328,018
  • 92
  • 813
  • 992
  • 29
    This should be the [Wikipedia explanation](http://en.wikipedia.org/wiki/Pseudo-polynomial_time)... – Sandro Meier Jul 17 '14 at 09:49
  • Your hope has not been in vain. – embert Dec 19 '14 at 14:16
  • `n mod i` is not O(1) – Ben Voigt Jan 03 '15 at 00:51
  • Just a follow up question - why are we compelled to write the number in binary? Why wouldn't writing it in unary help? – Sam Keays Mar 12 '15 at 22:52
  • @SamKeays You actually can write numbers out in unary! In fact, if you do, you sometimes turn problems that are NP-hard into problems solvable in polynomial time. As an example, the knapsack problem with unary inputs is solvable in polynomial time. You sometimes hear the term "weak NP hardness" to refer to problems that are NP-hard when the inputs are in binary but in P if the inputs are in unary. You also sometimes hear "strong NP hardness" to refer to problems that are hard in either unary or binary. Hope this helps! – templatetypedef Mar 12 '15 at 23:15
  • @templatetypedef Fantastic answer! Keep up with the good work. – Miljen Mikic Jul 08 '15 at 10:32
  • @templatetypedef IMO, the "right" definition of "polynomial time" has more to do with space complexity than with time complexity, because it is related to technical facts of how computers store numbers and symbols. Of course this might contribute to the time complexity, but... – nbro Sep 03 '15 at 11:20
  • 4
    Why is `isPrime`'s complexity estimated as O(n^4) and not simply O(n)? I don't get it. Unless the complexity of `n mod i` is O(n^3) .... which surely isn't. – fons Jul 02 '16 at 10:42
  • The size-based definition of complexity makes it depend on how "compressed" the input representation is. How's that useful? As mentioned above, due to the exponential _compression_ in [positional notation](https://en.wikipedia.org/wiki/Positional_notation) all O(2^x) problems all of a sudden become O(x), where n is the size of an input number. It seems like a mere convention leading to confusion. – fons Jul 02 '16 at 11:30
  • I've the same doubt as @fons about O(n^4) , can someone pls explain that? – Nobody Jun 27 '17 at 21:39
  • 5
    @Nobody Normally we think of the cost of modding two numbers as O(1), but when you're dealing with arbitrarily-large numbers, the cost of doing the multiplication increases as a function of the size of the numbers themselves. To be extremely conservative, I made the claim that you can compute modding by a number that's less than n as O(n^3), which is a gross overcounting but still not too bad. – templatetypedef Jun 27 '17 at 23:19
  • @templatetypedef - For the prime numbers pseudocode you wrote, won't the time complexity in pseudopolynomial time be O(10^(4x)) rather than O(2^(4x)) Because the it takes log10 n digits to represent n. – Andrew Flemming Jul 20 '17 at 18:36
  • 1
    @AndrewFlemming It depends on how the number is represented in memory. I was assuming we were using a standard binary representation, where we'd need log_2 n bits to represent the number. You're right that changing the underlying representation will change the runtime as a function of the size of the input, though. – templatetypedef Jul 20 '17 at 18:42
  • The length of the input in bits is x = \Theta(N log(k)). Then running time would be O((x / logx) + k). Can you please explain how the running time is exponential with respect to x? – namesake22 Jan 08 '18 at 16:47
  • 1
    Picking O(n^3) for `n mod i` is overly conservative. The timing of `mod` is a function of the number of bits in `n`, not the `n` itself, so it should be O((log n)^3). – Sergey Kalinichenko May 05 '18 at 11:53
  • I'd like to thank you for saving my computional complexity exam. Big up. – LaurensVijnck May 27 '18 at 22:24
  • let's take a dyn.programming solution for 0/1 knapsack problem with time complexity `O(n * C)` (`C` is knapsack capacity). well, I understand that this complexity exponentially depends on the number of bits, but how does that affect the total number of operations ? we need to fill `n * C` table cells, in other words we need to accomplish `n * C` operations and this expression depends polynomially on both `n` and `C` – mangusta Apr 25 '19 at 08:48
3

Pseudo-polynomial time complexity means polynomial in the value/magnitude of input but exponential in the size of input.

By size we mean the number of bits required to write the input.

From the pseudo-code of knapsack, we can find the time complexity to be O(nW).

// Input:
// Values (stored in array v) 
// Weights (stored in array w)
// Number of distinct items (n) //
Knapsack capacity (W) 
for w from 0 to W 
    do   m[0, w] := 0 
end for  
for i from 1 to n do  
        for j from 0 to W do
               if j >= w[i] then 
                      m[i, j] := max(m[i-1, j], m[i-1, j-w[i]] + v[i]) 
              else 
                      m[i, j] := m[i-1, j]
              end if
       end for 
end for

Here, W is not polynomial in the length of the input though, which is what makes it pseudo-polynomial.

Let s be number of bits required to represent W

i.e. size of input= s =log(W) (log= log base 2)
-> 2^(s)=2^(log(W))
-> 2^(s)=W  (because  2^(log(x)) = x)

Now, running time of knapsack= O(nW) = O(n * 2^s) which is not polynomial.

Adi agarwal
  • 170
  • 1
  • 9