2016-06-07 2 views
5

У меня есть код с высокой степенью чувствительности на сервере Node.js, который должен учитывать комбинации. Из this SO answer, я использовал эту простую рекурсивную функцию для вычисления п выбрать к:Эффективное вычисление n выбирает k в Node.js

function choose(n, k) { 
    if (k === 0) return 1; 
    return (n * choose(n-1, k-1))/k; 
} 

Тогда, так как мы все знаем, итерация почти всегда быстрее, чем рекурсии, я написал эту функцию, основанную на multiplicative formula:

function choosei(n,k){ 
    var result = 1; 
    for(var i=1; i <= k; i++){ 
     result *= (n+1-i)/i; 
    } 
    return result; 
} 

Я пробежал несколько benchmarks на моей машине. Вот результаты только один из них:

Recursive x 178,836 ops/sec ±7.03% (60 runs sampled) 
Iterative x 550,284 ops/sec ±5.10% (51 runs sampled) 
Fastest is Iterative 

Результаты последовательно показали, что итерационный метод действительно около 3 до 4 раз быстрее, чем рекурсивный метод в Node.js (по крайней мере, на моей машине).

Это вероятно достаточно быстро для моих потребностей, но есть ли способ сделать это быстрее? Мой код должен вызывать эту функцию очень часто, иногда с довольно большими значениями n и k, поэтому чем быстрее, тем лучше.

EDIT

После запуска еще несколько тестов с le_m-х и решений Майка, оказывается, что в то время как значительно быстрее, чем итеративный метод я предложил метод Майка с использованием треугольника Паскаля, кажется, немного быстрее, чем журнал le_m в таблица метод.

Recursive x 189,036 ops/sec ±8.83% (58 runs sampled) 
Iterative x 538,655 ops/sec ±6.08% (51 runs sampled) 
LogLUT x 14,048,513 ops/sec ±9.03% (50 runs sampled) 
PascalsLUT x 26,538,429 ops/sec ±5.83% (62 runs sampled) 
Fastest is PascalsLUT 

И поиск метод логарифмический был около 26-28 раз быстрее, чем итеративный метод в моих тестах, и метод с использованием треугольника Паскаля был около 1,3 до 1,8 раза быстрее, чем логарифмический смотреть вверх метод.

Обратите внимание, что я последовал предложению le_m по предварительно вычисления логарифмов с более высокой точностью, используя mathjs, затем превращают их в регулярные JavaScript Number с (которые всегда double-precision 64 bit floats).

+2

Меморандум может быть самым быстрым выбором, если пространство не является для вас проблемой. – Godfather

+0

Из комментария, на который вы ссылались, и на странице [wikipedia] (https://en.wikipedia.org/wiki/Binomial_coefficient#Factorial_formula): 'if (k> n/2) return select (n, nk) ; '- что поможет, когда и' n' и 'k' большие, но дополнительная ветвь может замедлить общее выполнение. – Bergi

+2

@ Godfather @NanoWizard, и если у вас нет места для моделирования результатов selecti, вы можете получить хороший прирост производительности за счет модификации факториалов. Вы даже можете использовать метод динамического программирования для вычисления факториала 'fact (n) = max_known_fact_value (n) * [i для evey int для max_known_fact_int (n) до n]', что сэкономит значительное время – gbtimmon

ответ

4

Никогда не вычисляйте факториалы, они растут слишком быстро. Вместо этого вычислите нужный результат. В этом случае вам нужны биномиальные числа, которые имеют невероятно простую геометрическую конструкцию: вы можете построить pascal's triangle, как вам нужно, и сделать это с помощью простой арифметики.

Начать с [1] и [1,1]. Следующая строка имеет [1] в начале, [1 + 1] в середине и [1] в конце: [1,2,1]. Следующая строка: [1] в начале, сумма первых двух членов в пятне 2, сумма следующих двух членов в третьем месте и [1] в конце: [1,3,3,1]. Следующая строка: [1], затем 1 + 3 = 4, затем 3 + 3 = 6, затем 3 + 1 = 4, затем [1] в конце и т. Д. И т. Д. Как вы можете видеть, нет факториалов, логарифмов или даже умножений: просто супер быстрое добавление с чистыми целыми числами. Так просто, вы можете построить массивную таблицу поиска вручную.

И вы должны.

Никогда не вычисляйте в коде то, что вы можете вычислить вручную и просто hardcode, в этом случае выписывание таблицы до n = 20 абсолютно тривиально, и вы можете просто использовать это как свой «начальный LUT» и вероятно, даже не нужны эти высокие ряды.

Но, если они вам понадобятся, или больше, потому что вы не можете построить бесконечную таблицу поиска, которую вы ставите на компромисс: вы начинаете с заданного LUT и функцию, которая может «заполнить» ее до определенного срока. нужно, чтобы этого не было:

module.exports = (function() { 
    // step 1: a basic LUT with a few steps of Pascal's triangle 
    var binomials = [ 
    [1], 
    [1,1], 
    [1,2,1], 
    [1,3,3,1], 
    [1,4,6,4,1], 
    [1,5,10,10,5,1], 
    [1,6,15,20,15,6,1], 
    [1,7,21,35,35,21,7,1], 
    [1,8,28,54,70,54,28,8,1], 
    ... 
    ]; 

    // step 2: a function that builds out the LUT if it needs to. 
    function binomial(n,k) { 
    while(n >= binomials.length) { 
     let s = binomials.length; 
     let nextRow = []; 
     nextRow[0] = 1; 
     for(let i=1, prev=s-1; i<s; i++) { 
     nextRow[i] = binomials[prev][i-1] + binomials[prev][i]; 
     } 
     nextRow[s] = 1; 
     binomials.push(nextRow); 
    } 
    return binomials[n][k]; 
    } 

    return binomial; 
}()); 

Поскольку это массив целых чисел, объем памяти крошечный. Для большой работы с биномами мы реально не нуждаемся в более чем двух байтах на целое число, что делает эту таблицу минут: нам не нужно больше 2 байтов, пока вам не понадобятся биномы выше n = 19, и полная таблица поиска до n = 19 занимает до 380 байтов. Это ничто по сравнению с остальной частью вашей программы. Даже если мы допустим 32-битные int, мы можем получить до n = 35 всего лишь 2380 байт.

Таким образом, поиск выполняется быстро: либо O (константа) для ранее вычисленных значений, (n * (n + 1))/2 шага, если у нас вообще нет LUT (в большой нотации O это будет O (n²), но большая нотация O почти никогда не является правильной мерой сложности), и где-то между терминами, которые нам нужны, которых еще нет в LUT. Запустите некоторые тесты для своего приложения, которые расскажут вам, насколько велика ваша начальная точка, просто жесткий код, который (серьезно, это константы, это ровно вид значений, который должен быть жестко закодирован), и держите генератор на всякий случай.

Однако, помните, что вы в JavaScript земли, и вы ограничены в JavaScript числового типа: integers only go up to 2^53, за что целое имущество (каждый n имеет отчетливый m=n+1 таким образом, что m-n=1) не гарантируется. Это вряд ли когда-либо будет проблемой, хотя: однажды мы достигнем этого предела, мы имеем дело с биномиальными коэффициентами, которые вы никогда не должны использовать.

6

Следующий алгоритм имеет время выполнения сложность O (1) дается линейная справочная таблица лога-факториалы с пространственно-сложностью О (п).

Ограничение п и K в диапазоне [0, 1000] имеет смысл, так как binomial(1000, 500) уже опасно близко к Number.MAX_VALUE. Поэтому нам нужна справочная таблица размером 1000.

В современном движке JavaScript компактный массив из n чисел имеет размер n * 8 байтов. Таким образом, полная таблица поиска потребует 8 килобайт памяти. Если мы ограничим наш вход диапазоном [0, 100], таблица будет занимать только 800 байт.

var logf = [0, 0, 0.6931471805599453, 1.791759469228055, 3.1780538303479458, 4.787491742782046, 6.579251212010101, 8.525161361065415, 10.60460290274525, 12.801827480081469, 15.104412573075516, 17.502307845873887, 9.987214495661885, 22.552163853123425, 25.19122118273868, 27.89927138384089, 30.671860106080672, 33.50507345013689, 36.39544520803305, 39.339884187199495, 42.335616460753485, 45.38013889847691, 48.47118135183523, 51.60667556776438, 54.78472939811232, 58.00360522298052, 61.261701761002, 64.55753862700634, 67.88974313718154, 71.25703896716801, 74.65823634883016, 78.0922235533153, 81.55795945611504, 85.05446701758152, 88.58082754219768, 92.1361756036871, 95.7196945421432, 99.33061245478743, 102.96819861451381, 106.63176026064346, 110.32063971475739, 114.0342117814617, 117.77188139974507, 121.53308151543864, 125.3172711493569, 129.12393363912722, 132.95257503561632, 136.80272263732635, 140.67392364823425, 144.5657439463449, 148.47776695177302, 152.40959258449735, 156.3608363030788, 160.3311282166309, 164.32011226319517, 168.32744544842765, 172.3527971391628, 176.39584840699735, 180.45629141754378, 184.53382886144948, 188.6281734236716, 192.7390472878449, 196.86618167289, 201.00931639928152, 205.1681994826412, 209.34258675253685, 213.53224149456327, 217.73693411395422, 221.95644181913033, 226.1905483237276, 230.43904356577696, 234.70172344281826, 238.97838956183432, 243.2688490029827, 247.57291409618688, 251.8904022097232, 256.22113555000954, 260.5649409718632, 264.9216497985528, 269.2910976510198, 273.6731242856937, 278.0675734403661, 282.4742926876304, 286.893133295427, 291.3239500942703, 295.76660135076065, 300.22094864701415, 304.6868567656687, 309.1641935801469, 313.65282994987905, 318.1526396202093, 322.66349912672615, 327.1852877037752, 331.7178871969285, 336.26118197919845, 340.815058870799, 345.37940706226686, 349.95411804077025, 354.5390855194408, 359.1342053695754, 363.73937555556347]; 
 

 
function binomial(n, k) { 
 
    return Math.exp(logf[n] - logf[n-k] - logf[k]); 
 
} 
 

 
console.log(binomial(5, 3));

Объяснение

Начиная с оригинальным итеративным алгоритмом, мы сначала заменить продукт на сумму логарифмов:

function binomial(n, k) { 
    var logresult = 0; 
    for (var i = 1; i <= k; i++) { 
     logresult += Math.log(n + 1 - i) - Math.log(i); 
    } 
    return Math.exp(logresult); 
} 

Нашего цикл в настоящее время суммы по k терминов. Если мы переставим сумму, мы можем легко увидеть, что мы суммируем по последовательным логарифмам log(1) + log(2) + ... + log(k) и т. Д., Которые мы можем заменить на sum_of_logs(k), который фактически идентичен log(n!). Предварительно вычитая эти значения и сохраняя их в нашем справочном таблице logf, выведем вышеописанный алгоритм с одним слоем.

Вычисление таблицы поиска:

Я рекомендую предварительное вычисление в справочной таблице-с более высокой точностью и преобразованием результирующих элементов в 64-битных поплавки. Если вам не нужно, что немного дополнительной точность или вы хотите, чтобы запустить этот код на стороне клиента, используйте:

var size = 1000, logf = new Array(size); 
logf[0] = 0; 
for (var i = 1; i <= size; ++i) logf[i] = logf[i-1] + Math.log(i); 

Численной точность:

С помощью лог-факториала, мы избегаем точности проблемы, присущие хранению необработанных факториалов.

Мы могли бы даже использовать Stirling's approximation для log(n!) вместо поиска стола и все еще получить 12 значащих цифр для выше вычислений как в перспективе времени и пространства сложности O (1):

function logf(n) { 
 
    return n === 0 ? 0 : (n + .5) * Math.log(n) - n + 0.9189385332046728 + 0.08333333333333333/n - 0.002777777777777778 * Math.pow(n, -3); 
 
} 
 

 
function binomial(n , k) { 
 
    return Math.exp(logf(n) - logf(n - k) - logf(k)); 
 
} 
 

 
console.log(binomial(1000, 500)); // 2.7028824094539536e+299

+0

Отлично. Всегда ли он дает целочисленные значения? – Kundor

+0

Арифметические операции выполняются с 64-битными значениями с плавающей запятой, поэтому вам нужно будет e. г. форматируйте результат перед печатью, обрезая незначительные дробные цифры. –

Смежные вопросы