Вычисление факториала большая сложность

я столкнулся с проблемой, где мне нужно было рассчитать значения очень больших факториалов. Я решил эту проблему на 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 фактор.