Qu'est-ce que temps pseudopolynomial ? En quoi diffère-t-il du temps polynomial? Certains algorithmes qui s'exécutent en temps pseudopolynomial ont des durées d'exécution comme O(nW) (pour le /1 Knapsack Problem ) ou O (√n) (pour division d'essai ); pourquoi cela ne compte-t-il pas comme du temps polynomial?
Pour comprendre la différence entre le temps polynomial et le temps pseudopolynomial, nous devons commencer par formaliser ce que signifie le "temps polynomial".
L’intuition courante pour le temps polynomial est "temps O (nk) pour certains k. "Par exemple, selection sort s'exécute dans le temps O (n2), qui est le temps polynomial, tandis que la résolution par force brute TSP prend le temps O (n · n!), qui n'est pas le temps polynomial.
Ces temps d'exécution se réfèrent tous à une variable n qui suit la taille de l'entrée. Par exemple, dans le tri par sélection, n fait référence au nombre d'éléments dans le tableau, tandis que dans TSP n fait référence au nombre de nœuds dans le graphique. Afin de normaliser la définition de ce que "n" signifie réellement dans ce contexte, la définition formelle de la complexité temporelle définit la "taille" d'un problème comme suit:
La taille de l'entrée d'un problème est le nombre de bits requis pour écrire cette entrée.
Par exemple, si l'entrée d'un algorithme de tri est un tableau d'entiers 32 bits, la taille de l'entrée serait 32n, où n est le nombre d'entrées du tableau. Dans un graphique à n nœuds et m arêtes, l'entrée peut être spécifiée comme une liste de tous les nœuds suivie d'une liste de tous les arêtes, ce qui nécessiterait Ω (n + m) bits.
Compte tenu de cette définition, la définition formelle du temps polynomial est la suivante:
Un algorithme s'exécute en temps polynomial si son temps d'exécution est O (xk) pour une constante k, où x désigne le nombre de bits d'entrée donnés à l'algorithme.
Lorsque vous travaillez avec des algorithmes qui traitent des graphiques, des listes, des arbres, etc., cette définition correspond plus ou moins à la définition conventionnelle. Par exemple, supposons que vous disposiez d'un algorithme de tri qui trie les tableaux d'entiers 32 bits. Si vous utilisez quelque chose comme le tri par sélection pour ce faire, le runtime, en fonction du nombre d'éléments d'entrée dans le tableau, sera O (n2). Mais comment n, le nombre d'éléments dans le tableau d'entrée, correspond-il au nombre de bits d'entrée? Comme mentionné précédemment, le nombre de bits d'entrée sera x = 32n. Par conséquent, si nous exprimons le temps d'exécution de l'algorithme en termes de x plutôt que de n, nous obtenons que le temps d'exécution est O (x2), et donc l'algorithme s'exécute en temps polynomial.
De même, supposons que vous fassiez recherche en profondeur d'abord sur un graphe, ce qui prend le temps O (m + n), où m est le nombre d'arêtes dans le graphe et n est le nombre de nœuds. Comment cela se rapporte-t-il au nombre de bits d'entrée fournis? Eh bien, si nous supposons que l'entrée est spécifiée comme une liste d'adjacence (une liste de tous les nœuds et bords), alors comme mentionné précédemment, le nombre de bits d'entrée sera x = Ω (m + n). Par conséquent, le temps d'exécution sera O (x), donc l'algorithme s'exécute en temps polynomial.
Les choses tombent cependant en panne lorsque nous commençons à parler d'algorithmes qui fonctionnent sur des nombres. Examinons le problème de tester si un nombre est premier ou non. Étant donné un nombre n, vous pouvez tester si n est premier en utilisant l'algorithme suivant:
function isPrime(n):
for i from 2 to n - 1:
if (n mod i) = 0, return false
return true
Quelle est donc la complexité temporelle de ce code? Eh bien, cette boucle interne s'exécute O(n) fois et fait à chaque fois une certaine quantité de travail pour calculer n mod i (en tant que limite supérieure vraiment conservatrice, cela peut certainement être fait dans le temps O ( n3)). Par conséquent, cet algorithme global s'exécute dans le temps O (n4) et peut-être beaucoup plus rapidement.
En 2004, trois informaticiens ont publié un article intitulé PRIMES est en P donnant un algorithme polynomial pour tester si un nombre est premier. Il a été considéré comme un résultat historique. Alors, quel est le gros problème? N'avons-nous pas déjà un algorithme polynomial pour cela, à savoir celui ci-dessus?
Malheureusement non. Rappelez-vous, la définition formelle de la complexité temporelle parle de la complexité de l'algorithme en fonction du nombre de bits d'entrée. Notre algorithme fonctionne dans le temps O (n4), mais qu'est-ce que cela en fonction du nombre de bits d'entrée? Eh bien, écrire le nombre n prend O (log n) bits. Par conséquent, si nous laissons x le nombre de bits requis pour écrire l'entrée n, le temps d'exécution de cet algorithme est en fait O (24x), qui est pas un polynôme en x.
C'est le cœur de la distinction entre le temps polynomial et le temps pseudopolynomial. D'une part, notre algorithme est O (n4), qui ressemble à un polynôme, mais d'un autre côté, selon la définition formelle du temps polynomial, ce n'est pas du temps polynomial.
Pour avoir une idée de pourquoi l'algorithme n'est pas un algorithme polynomial, pensez aux points suivants. Supposons que je veuille que l'algorithme doive faire beaucoup de travail. Si j'écris une entrée comme celle-ci:
10001010101011
alors cela prendra un certain temps, disons T
, pour terminer. Si j'ajoute maintenant un seul bit à la fin du numéro, comme ceci:
100010101010111
Le runtime sera désormais (dans le pire des cas) 2T. Je peux doubler la quantité de travail que l'algorithme fait simplement en ajoutant un bit de plus!
Un algorithme s'exécute en temps pseudopolynomial si le temps d'exécution est un polynôme dans la valeur numérique de l'entrée, plutôt qu'en le nombre de bits requis pour le représenter. Notre algorithme de test principal est un algorithme de temps pseudopolynomial, car il s'exécute dans le temps O (n4), mais ce n'est pas un algorithme polynomial car en fonction du nombre de bits x requis pour écrire l'entrée, le runtime est O (24x). La raison pour laquelle le document "PRIMES est en P" était si important était que son exécution était (à peu près) O (log12 n), qui en fonction du nombre de bits est O (x12).
Alors, pourquoi est-ce important? Eh bien, nous avons de nombreux algorithmes temporels pseudopolynomiaux pour factoriser les entiers. Cependant, ces algorithmes sont, techniquement parlant, des algorithmes à temps exponentiel. Ceci est très utile pour la cryptographie: si vous souhaitez utiliser le cryptage RSA, vous devez être sûr que nous ne pouvons pas factoriser facilement les nombres. En augmentant le nombre de bits dans les nombres à une valeur énorme (disons, 1024 bits), vous pouvez rendre le temps que l'algorithme de factorisation pseudopolynomial-time doit prendre si grand qu'il serait complètement et totalement impossible à factoriser le Nombres. Si, par contre, nous pouvons trouver un algorithme de factorisation temporelle - polynôme, ce n'est pas nécessairement le cas. L'ajout de bits supplémentaires peut entraîner une augmentation considérable du travail, mais la croissance ne sera qu'une croissance polynomiale, pas une croissance exponentielle.
Cela dit, dans de nombreux cas, les algorithmes temporels pseudopolynomiaux sont parfaitement adaptés car la taille des nombres ne sera pas trop grande. Par exemple, tri de comptage a le temps d'exécution O (n + U), où U est le plus grand nombre du tableau. Il s'agit d'un temps pseudopolynomial (car la valeur numérique de U nécessite O (log U) pour écrire, donc le temps d'exécution est exponentiel dans la taille d'entrée). Si nous contraignons artificiellement U pour que U ne soit pas trop grand (disons, si nous laissons U égal à 2), alors le temps d'exécution est O (n), qui est en fait le temps polynomial. Voici comment tri radix fonctionne: en traitant les nombres un bit à la fois, le temps d'exécution de chaque tour est O (n), donc le temps d'exécution global est O (n log U). En fait, cela c'est temps polynomial, car l'écriture de n nombres pour trier utilise Ω (n) bits et la valeur de log U est directement proportionnelle au nombre de bits requis pour écrire la valeur maximale dans le tableau.
J'espère que cela t'aides!
La complexité temporelle pseudo-polynomiale signifie polynôme dans la valeur/amplitude de l'entrée mais exponentielle dans la taille de l'entrée.
Par taille, nous entendons le nombre de bits requis pour écrire l'entrée.
À partir du pseudo-code du sac à dos, nous pouvons trouver que la complexité temporelle est 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
Ici, W n'est pas polynomial dans la longueur de l'entrée, ce qui le rend pseudo-polynomial.
Soit s le nombre de bits requis pour représenter 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)
À présent, running time of knapsack
= O(nW) = O (n * 2 ^ s) qui n'est pas polynomial.