Вычисление большой факториальной временной сложности
Я столкнулся с проблемой, когда мне нужно было рассчитать значения очень больших факториалов. Я решил эту проблему на С++ двумя разными способами, но хочу знать, действительно ли мой анализ сложности.
В любом из методов я представляю очень большие числа в виде векторов, где v[0]
представляет наименее значащую цифру, а значение последнего индекса представляет собой наиболее значимую цифру. Код версии 1 можно найти в этом gist.
Учитывая приведенный выше код, кажется, что multiplyVectorByInteger()
есть O(log(n*k))
, где n
- заданное целое число, а k
- это число, представленное вектором. Моя логика заключается в том, что мы будем делать несколько шагов пропорционально длине результирующего числа n*k
, чтобы создать вектор, представляющий n*k
. Длина n*k
равна O(log(n*k))
Некоторые из шагов будут выполняться в цикле for, другие - в цикле while.
В этой программе для поиска больших факториалов, всякий раз, когда мы называем multiplyVectorByInteger()
, мы будем передавать целое число n
и векторное представление (n-1)!
. Это означает, что если мы хотим найти 6!
, мы передадим целое число 6
и векторное представление 5!
. Функция вернет векторное представление 6!
. Используя предыдущую информацию, я считаю, что сложность - это O(log(i!))
, где я - это переданное целое число. Чтобы найти большие факториалы, мы должны назвать этот метод O(n)
times, где n
- факториал, который мы пытаемся найти. Наша накопленная логика будет выглядеть так:
1! = 1!
1!*2 = 2!
2!*3 = 3!
3!*4 = 4!
...
(n-1)!*n = n!
Так как на каждом уровне мы вычисляем i!
, мы последовательно выполняем шаги O(log(i!))
на каждом уровне. Суммирование, чтобы показать это, выглядит следующим образом:
![sum1]()
Моя логика от перехода от второго суммирования к нотации Big-Oh выглядит следующим образом: нарушая это, получаем следующее:
1log(1) + 2log(2) + 3log(3) + ... + nlog(n)
Очевидно, что мы получаем O(n^2)
члены log(1) + log(2) + ... + log(n)
. Правила журнала напоминают нам, что log(a) + log(b) = log(ab)
, что означает, что члены журнала в этом случае сворачиваются на log(n!)
. Таким образом, имеем O(n^2)log(n!)
.
Это создаст общую временную сложность этой программы O(n^2log(n!))
. Правильно ли этот анализ?
Максимальная временная сложность версии
Чтобы анализировать сложность, я хочу взглянуть на то, что кажется менее эффективным решением. Предположим, мы изменили нашу функцию multiplyVectorByInteger()
так, чтобы вместо умножения векторного представления k
на целое число n
в O(log(n!))
времени для создания n!
, новая функция multiplyVectorByIntegerNaive()
добавляет векторное представление числа вместе в общей сложности n
раз.
multiplyVectorByIntegerNaive()
существует в gist. В нем используется функция addVectors()
, сложность которой O(n)
где n размер большего из двух векторов.
Ясно, что мы по-прежнему вызываем эту новую функцию умножения n
раз, но нам нужно увидеть, изменилась ли сложность. Например, учитывая целое число 6
и векторное представление 5!
, добавим 5! + 5! + 5! + 5! + 5! + 5!
, чтобы получить 6*5! = 6!
. Если заданное целое число для нашей функции умножения i
, то ясно, что мы делаем дополнения i-1
. Мы можем перечислять шаги для предыдущего примера вызова нашей наивной функции умножения.
5! + 5!
2*5! + 5!
3*5! + 5!
4*5! + 5!
5*5! + 5!
Списание полного суммирования должно теперь давать:
![sum2]()
Похоже, что асимптотическая сложность обоих методов одинакова, если мои вычисления точны. Это правда?
Ответы
Ответ 1
Сложность функции в представленном вами значении O(log10n!)
, где n
- это номер, который вы передаете методу.
Обоснование этого очевидно из первой части кода:
for (int i = 0; i < numVector.size(); ++i) {
returnVec[i] = (numVector[i]*n + carry) % 10;
carry = (numVector[i]*n + carry) / 10;
}
Переданный numVector
представляет (n - 1)!
. т.е. содержит все цифры, составляющие это число. Однако длина этого числа просто ⌈log10((n-1)!)⌉
. Это можно увидеть на простом примере:
если (n-1)!
равно 100, тогда длина numVector
будет 3, которая будет такой же, как ⌈log10100⌉ = 3
.
То же самое относится и к циклу while:
while (carry) {
returnVec.push_back(carry%10);
carry /= 10;
}
Так как значение carry
не будет больше, чем n
(вы можете это доказать самостоятельно), то максимальное количество раз, в течение которого этот цикл будет работать, также не будет больше, чем ⌈log10n!⌉
, тогда общая сумма сложность функции эквивалентна O(log10n!)
.
Поэтому для вычисления k!
сложность вашего кода (включая main) будет O(klog10k!)
Наивная версия
Для наивной версии единственное, что изменилось, это то, что теперь метод вручную переходит через умножение в форме сложения. Это то, что другая версия пропущена путем явного умножения каждого значения на n
(numVector[i]*n + carry)
Это увеличивает сложность функции до O(klog10n!)
, где k! = n
и, следовательно, сложность всего кода теперь O(k2log10k!)
Ответ 2
Умножая число k-цифр на целое число или добавляя два k-значных числа, время занимает пропорционально k.
Следовательно, в многократной версии общая рабочая нагрузка
Sum[i=1,n]: log(i!) ~ Sum[i=1,n]: i.log(i) ~ n²log(n)
В дополнительной версии
Sum[i=1,n]: i.log(i!) ~ Sum[i=1,n]: i².log(i!) ~ n³.log(n)
Эти результаты могут быть установлены с помощью приближения Стирлинга и интеграла вместо суммирования
Int x.log(x) dx = x²(log(x)/2 - 1/4)
Int x².log(x) dx = x³(log(x)/3 - 1/9)
Как и следовало ожидать, существует дополнительный фактор n
.