рассчитать время для обработки комбинаций javascript

#javascript

#javascript

Вопрос:

У меня есть эта функция из:https://rosettacode.org/wiki/Combinations#ES6

В моей среде console.log(show(comb(3,15))); (такой же, как в приведенном ниже фрагменте) занимает приблизительно. 4 секунды на обработку

Если я попытаюсь console.log(show(comb(3,16))); , это займет около 16 секунд

Если я попытаюсь console.log(show(comb(3,17))); , это займет около 90 секунд

Но если бы я попытался: console.log(show(comb(3,20))); После одного часа процесс еще не завершен, и я остановил его.

Вопрос в том:

Как заранее рассчитать время для обработки comb(3,50) или comb(3,80) ?

 (() => {
    'use strict';

 
    // COMBINATIONS -----------------------------------------------------------
 
    // comb :: Int -> Int -> [[Int]]
    const comb = (m, n) => combinations(m, enumFromTo(0, n - 1));
 
    // combinations :: Int -> [a] -> [[a]]
    const combinations = (k, xs) =>
        sort(filter(xs => k === xs.length, subsequences(xs)));
 
 
    // GENERIC FUNCTIONS -----------------------------------------------------
 
    // cons :: a -> [a] -> [a]
    const cons = (x, xs) => [x].concat(xs);
 
    // enumFromTo :: Int -> Int -> [Int]
    const enumFromTo = (m, n) =>
        Array.from({
            length: Math.floor(n - m)   1
        }, (_, i) => m   i);
 
    // filter :: (a -> Bool) -> [a] -> [a]
    const filter = (f, xs) => xs.filter(f);
 
    // foldr (a -> b -> b) -> b -> [a] -> b
    const foldr = (f, a, xs) => xs.reduceRight(f, a);
 
    // isNull :: [a] -> Bool
    const isNull = xs => (xs instanceof Array) ? xs.length < 1 : undefined;
 
    // show :: a -> String
    const show = x => JSON.stringify(x) //, null, 2);
 
    // sort :: Ord a => [a] -> [a]
    const sort = xs => xs.sort();
 
    // stringChars :: String -> [Char]
    const stringChars = s => s.split('');
 
    // subsequences :: [a] -> [[a]]
    const subsequences = xs => {
 
        // nonEmptySubsequences :: [a] -> [[a]]
        const nonEmptySubsequences = xxs => {
            if (isNull(xxs)) return [];
            const [x, xs] = uncons(xxs);
            const f = (r, ys) => cons(ys, cons(cons(x, ys), r));
 
            return cons([x], foldr(f, [], nonEmptySubsequences(xs)));
        };
 
        return nonEmptySubsequences(
            (typeof xs === 'string' ? stringChars(xs) : xs)
        );
    };
 
    // uncons :: [a] -> Maybe (a, [a])
    const uncons = xs => xs.length ? [xs[0], xs.slice(1)] : undefined;
 
 
    // TEST -------------------------------------------------------------------
    // return show(
        // comb(3, 5)
    // );
    
    console.log(show(comb(3,15)));
})();  

Ответ №1:

Используйте биномиальные коэффициенты. Время для обработки comb(3,n) равно n choose 3 тому, n*(n-1)*(n-2)/6 следовательно O(n^3) , которое получается. Например, увеличение n в 10 раз должно увеличить время выполнения примерно в 1000 раз.

20 choose 3 составляет всего 1140, поэтому, если для их генерации требуется более часа, рассматриваемый алгоритм не особенно хорош. Кроме того, разрыв между 20 choose 3 и 17 choose 3 не настолько велик, чтобы это действительно объясняло разницу во времени. Таким образом, асимптотический анализ дает лишь предположение о том, что происходит. Фактическое время выполнения, похоже, намного хуже.

Комментарии:

1. Обратите внимание, что время, которое потребуется, является идеализированным и относительно компьютера, на котором вы работаете. Вы не сможете точно предсказать фактическое время (в секундах) для всех машин и контекстов, и будут другие одновременные требования к процессору, которые практически невозможно учесть. Также могут быть другие факторы, не рассмотренные здесь, такие как работа, необходимая для выделения памяти для хранения массива, что по-своему повлияет на время завершения и т.д.

2. @JDB Вы, конечно, правы. Математика говорит вам, что вы можете сделать не лучше, чем cubic, но плохой алгоритм может сделать его хуже.

3. @John Coleman Я не знаю, будет ли это плохо, но я должен выбрать ответ Скотта, потому что он также дал мне другую функцию намного лучше. В любом случае, большое вам спасибо. Ваш ответ был очень полезным и быстрым. Мои извинения

4. @MTK Вообще никаких проблем. Я только что поддержал ответ Скотта, который является более полным, чем мой.

Ответ №2:

Как сказал Джон Коулман, биномиальные коэффициенты могут дать вам относительное представление о том, сколько времени должны занимать различные прогоны.

Без анализа вашего кода приведенные вами цифры совершенно ясно показывают, что там что-то не так.

Более простая версия может выглядеть следующим образом:

 // combinations :: Int -> [a] -> [[a]]
const combinations = (m) => (ns) => (ns.length == 0 || m == 0)
  ? []
  : m == 1
    ? ns
    : combinations (m - 1) (ns .slice(1)) .map(xs => [ns[0]] .concat(xs))
        .concat (combinations (m) (ns.slice(1) ) )

// combinations (3) (['a', 'b', 'c', 'd', 'e'])
//   .map(ls => ls .join('') )
     //=> ["abc", "abd", "abe", "acd", "ace", "ade", "bcd", "bce", "bde", "cde"]

// range :: Int -> Int -> [Int]
const range = (lo) => (hi) => [...Array(hi - lo   1)].map((_, i) => i   lo)

// comb :: Int -> Int -> [[Int]]
const comb = (m, n) => combinations (m) (range (0) (n - 1))

console.clear()
const now = new Date();
console.log(comb(3, 20).length);
console.log(`time: ${new Date() - now} ms`)
// ~> 1140
// ~> time 2 ms  

combinations и combs будете вести себя так же, как у вас. Я не делаю никакой сортировки, сохраняя результирующие комбинации в том же порядке, что и в исходном списке.

Базовые случаи рекурсии просты. Когда список пуст, верните значение [] , и если m равно 0, верните список. Рекурсивный вариант просто повторяет и объединяет два варианта: те комбинации, которые включают первый элемент списка, и те, которые этого не делают. Этот второй простой, просто возвращающий combinations (m) (tail(ns)) . Первая также повторяется, вызывая combinations (m - 1) (tail(ns)) , но затем должна добавляться head(ns) к каждой. На самом деле я не использовал здесь функции head и tail , но, вероятно, использовал бы в производственном коде.

Обратите внимание, что comb(3, 20) это занимает всего миллисекунду или две.

Конечно, эта тяжелая рекурсия может иметь свою цену в JS, и я не думаю, что есть какой-либо простой способ сделать это приемлемым для устранения хвостового вызова. Но рекурсивная глубина вряд ли будет проблемой для комбинаций, которые вы действительно хотели бы вычислить.

Конечно, если бы вы хотели только посчитать комбинации, а не перечислять их, то код для генерации биномиальных коэффициентов должен быть намного проще.

Комментарии:

1. Это здорово, @Scott Sauyet ты «спас мою жизнь» (на сегодня …) 🙂