Щоб зрозуміти різницю між полиномним часом і псевдополіномічним часом, нам потрібно почати з формалізації того, що означає "поліноміальний час".
Загальна інтуїція для поліноміального часу - це "час O (n k ) для деякого k". Наприклад, сортування селекції працює у часі O (n 2 ), який є поліноміальним часом, тоді як TSP для розв'язання грубої сили вимагає часу O (n · n!), Який не є поліноміальним часом.
Усі умови виконання посилаються на деяку змінну n, яка відстежує розмір вхідного даних. Наприклад, у сортуванні виділення n посилається на кількість елементів масиву, тоді як у TSP n посилається на кількість вузлів у графі. Для стандартизації визначення того, що насправді означає «n» у цьому контексті, формальне визначення складності часу визначає «розмір» проблеми таким чином:
Розмір вводу для проблеми - це кількість бітів, необхідних для запису цього вводу.
Наприклад, якщо вхід до алгоритму сортування є масивом 32-бітових цілих чисел, то розмір вводу буде 32n, де n - кількість записів у масиві. У графіку з n вузлами та m ребрами вхід може бути вказаний у вигляді списку всіх вузлів з подальшим переліком усіх ребер, для яких знадобиться Ω (n + m) біт.
Враховуючи це визначення, формальне визначення поліноміального часу таке:
Алгоритм працює в поліноміальний час, якщо його час виконання дорівнює O (x k ) для деякої константи k, де x позначає кількість бітів вводу, заданого алгоритму.
Під час роботи з алгоритмами, які обробляють графіки, списки, дерева тощо, це визначення більш-менш узгоджується із загальноприйнятим визначенням. Наприклад, припустимо, що у вас є алгоритм сортування, який сортує масиви 32-бітних цілих чисел. Якщо для цього використовується щось подібне до вибору сортування, час виконання, як функція кількості вхідних елементів у масиві, буде O (n 2 ). Але як n, кількість елементів у вхідному масиві відповідає кількості бітів введення? Як згадувалося раніше, кількість бітів введення становитиме x = 32n. Тому, якщо ми виражаємо час виконання алгоритму через x, а не n, ми отримуємо, що час виконання - O (x 2 ), і алгоритм працює в поліноміальний час.
Аналогічно, припустимо, що ви здійснюєте перший поглиблений пошук на графіку, який вимагає часу O (m + n), де m - кількість ребер у графіку, а n - кількість вузлів. Як це пов'язано з кількістю заданих бітів? Добре, якщо припустити, що вхід вказаний як список суміжності (перелік усіх вузлів і ребер), то, як було сказано раніше, кількість бітів введення буде x = Ω (m + n). Тому час виконання буде O (x), тому алгоритм працює в поліноміальний час.
Однак речі руйнуються, коли ми починаємо говорити про алгоритми, які працюють на числах. Розглянемо проблему тестування, чи є число простим чи ні. Враховуючи число n, ви можете перевірити, чи n є простим, використовуючи наступний алгоритм:
function isPrime(n):
for i from 2 to n - 1:
if (n mod i) = 0, return false
return true
То яка часова складність цього коду? Добре, що внутрішній цикл працює O (n) разів, і кожен раз виконує певну кількість роботи для обчислення n mod i (як дійсно консервативна верхня межа, це, безумовно, можна зробити в часі O (n 3 )). Тому цей загальний алгоритм працює в часі O (n 4 ) і, можливо, набагато швидше.
У 2004 році троє вчених-комп'ютерів опублікували документ під назвою PRIMES в P, який дає алгоритм поліноміального часу для перевірки того, чи є число простим. Це вважалося знаковим результатом. То в чому ж велика справа? Хіба у нас вже немає алгоритму поліноміального часу для цього, а саме вищенаведеного?
На жаль, ми цього не робимо. Пам'ятайте, формальне визначення часової складності говорить про складність алгоритму як функції від кількості бітів введення. Наш алгоритм працює за часом O (n 4 ), але що це за функція від кількості вхідних бітів? Добре, виписуючи число n займає біти O (log n). Отже, якщо дозволити x кількість біт, необхідних для виписання вхідного n, час виконання цього алгоритму насправді O (2 4x ), що не є поліномом у x.
Це серце розрізнення між поліномним часом і псевдополіномічним часом. З одного боку, наш алгоритм - це O (n 4 ), який виглядає як многочлен, але, з іншого боку, за формальним визначенням часу многочлена це не поліноміально-час.
Щоб зрозуміти, чому алгоритм не є алгоритмом багаточленного часу, подумайте про наступне. Припустимо, я хочу, щоб алгоритм провів багато роботи. Якщо я випишу такий вхід:
10001010101011
тоді, скажімо T
, пройде деякий найгірший час, скажімо . Якщо я тепер додаю один біт до кінця числа, наприклад:
100010101010111
Час виконання зараз (в гіршому випадку) буде 2T. Я можу подвоїти кількість роботи, яку алгоритм виконує лише додавши ще один біт!
Алгоритм працює у псевдополіномічний час, якщо час виконання є деяким поліномом у числовому значенні введення , а не в кількості бітів, необхідних для його представлення. Наш алгоритм основного тестування - алгоритм псевдополіномічного часу, оскільки він працює в часі O (n 4 ), але це не алгоритм поліноміального часу, оскільки як функція від кількості бітів x, необхідних для виписання вводу, час виконання - O (2 4х ). Причиною того, що папір "PRIMES є в P" була настільки вагомою, що її час виконання був (приблизно) O (log 12 n), що в залежності від кількості бітів дорівнює O (x 12 ).
То чому це має значення? Ну, у нас є багато алгоритмів псевдополіноміального часу для розбиття цілих чисел. Однак ці алгоритми, технічно кажучи, є алгоритмами експоненціального часу. Це дуже корисно для криптографії: якщо ви хочете використовувати шифрування RSA, вам потрібно мати можливість довіряти, що ми не можемо легко визначити числа. Збільшуючи кількість бітів у числах до величезного значення (скажімо, 1024 біт), ви можете зробити кількість часу, який повинен зайняти алгоритм факторингу псевдополіномічного часу, настільки великий, що це було б цілком і нездійсненно чинним числа. Якщо, з іншого боку, ми можемо знайти алгоритм факторингу багаточленного часу, це не обов'язково. Додавання більшої кількості бітів може призвести до того, що робота зростатиме на багато, але зростання буде лише поліноміальним, а не експоненціальним.
Однак, у багатьох випадках алгоритми псевдополіномічного часу є ідеальними, оскільки розмір чисел не буде надто великим. Наприклад, сортування підрахунку має час виконання O (n + U), де U - найбільше число в масиві. Це псевдополіномічний час (тому що для числового значення U потрібні біти O (log U), щоб час списання було експоненціальним). Якщо ми штучно обмежуємо U так, що U не надто велике (скажімо, якщо дозволити U 2), то час виконання - O (n), що насправді є полиномним часом. Ось так працює radix сортування : обробляючи числа по одному біту, час виконання кожного раунду дорівнює O (n), тому загальний час виконання - O (n log U). Це насправді є поліноміальний час, тому що при написанні n чисел для сортування використовуються біти Ω (n), а значення журналу U прямо пропорційно кількості бітів, необхідних для виписання максимального значення в масиві.
Сподіваюся, це допомагає!