Вычисление факториала большая сложность
я столкнулся с проблемой, где мне нужно было рассчитать значения очень больших факториалов. Я решил эту проблему на C++ двумя разными способами, но хочу только знать, точен ли мой анализ сложности.
в любом методе я представляю очень большие числа в виде векторов, где v[0]
представляет наименее значимую цифру, а значение последнего индекса представляет наиболее значимую цифру. Код версии 1 можно найти в этом суть.
приведенный выше код, кажется multiplyVectorByInteger()
is O(log(n*k))
здесь n
- это целое число, а k
- число, представленное вектором. Моя логика заключается в том, что мы будем делать некоторое количество шагов, пропорциональных длине полученного числа n*k
для получения вектора, представляющего n*k
. Длина n*k
и O(log(n*k))
некоторые из шагов будут выполняться в цикле for, другие-в следующем цикле while.
In эта программа для поиска больших факториалов, когда мы называем multiplyVectorByInteger()
мы будем передавать целое число n
и векторное представление (n-1)!
. Это означает, если мы хотим найти 6!
, переходим в целое число 6
и векторное представление 5!
. Функция вернет векторное представление 6!
. Используя предыдущую информацию, я считаю, что могу сказать, что сложность O(log(i!))
где i-переданное целое число. Для того, чтобы найти большие факториалы, мы должны вызвать этот метод O(n)
раз n
это факториал, который мы пытаемся найти. Наша накопленная логика будет выглядеть так:
1! = 1!
1!*2 = 2!
2!*3 = 3!
3!*4 = 4!
...
(n-1)!*n = n!
так как на каждом уровне мы вычисляем i!
, следовательно, мы выступаем O(log(i!))
шагов на каждом уровне. Суммирование, чтобы показать это, выглядит следующим образом:
моя логика от прыжка со второго суммирования к большой-о нотации выглядит следующим образом...вырвавшись на свободу, мы получим ... следующий:
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
раза.
в этой суть. Он использует функцию 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!
написание полного суммирования должно теперь дайте:
похоже, что асимптотическая сложность обоих методов одинакова, учитывая, что мои вычисления точны. Это правда?
2 ответов
сложность функции в gist, которую вы предоставили, является 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!)
умножение K-разрядного числа на целое число или добавление двух K-разрядных чисел занимает время, пропорциональное k.
следовательно, в версии multiply общая рабочая нагрузка составляет
Sum[i=1,n]: log(i!) ~ Sum[i=1,n]: i.log(i) ~ n²log(n)
в версии add,
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
фактор.