Проецирование списка списков эффективно в F #
Мне нужно выполнить проектирование списка списков, который возвращает все комбинации с каждым элементом из каждого списка. Например:
projection([[1]; [2; 3]]) = [[1; 2]; [1; 3]].
projection([[1]; [2; 3]; [4; 5]]) = [[1; 2; 4]; [1; 2; 5]; [1; 3; 4]; [1; 3; 5]].
Я придумал функцию:
let projection lss0 =
let rec projectionUtil lss accs =
match lss with
| [] -> accs
| ls::lss' -> projectionUtil lss' (List.fold (fun accs' l ->
accs' @ List.map (fun acc -> acc @ [l]) accs)
[] ls)
match lss0 with
| [] -> []
| ls::lss' ->
projectionUtil lss' (List.map (fun l -> [l]) ls)
и тест:
#time "on";;
let N = 10
let fss0 = List.init N (fun i -> List.init (i+1) (fun j -> j+i*i+i));;
let fss1 = projection fss0;;
Теперь функция довольно медленная, а N = 10
требуется больше 10 секунд. Более того, я считаю, что решение неестественно, потому что мне приходится разбивать один и тот же список двумя разными способами. Любое предложение, как я могу улучшить производительность и читаемость функции?
Ответы
Ответ 1
Прежде всего, старайтесь избегать конкатенации списков (@), когда это возможно, поскольку это O (N) вместо O (1).
Я бы начал с (относительно) простого для понимания плана вычисления декартового внешнего произведения списков.
- Подготовить каждый элемент первого списка к каждому подсписку в декартовом произведении остальных списков.
- Позаботьтесь о базовом корпусе.
Первая версия:
let rec cartesian = function
| [] -> [[]]
| L::Ls -> [for C in cartesian Ls do yield! [for x in L do yield x::C]]
Это прямой перевод приведенных выше предложений в код.
Теперь ускорите это: вместо понимания списка используйте конкатенации и карты списка:
let rec cartesian2 = function
| [] -> [[]]
| L::Ls -> cartesian2 Ls |> List.collect (fun C -> L |> List.map (fun x->x::C))
Это можно сделать быстрее, вычислив списки по требованию через последовательность:
let rec cartesian3 = function
| [] -> Seq.singleton []
| L::Ls -> cartesian3 Ls |> Seq.collect (fun C -> L |> Seq.map (fun x->x::C))
Эта последняя форма - это то, что я использую сам, так как мне чаще всего нужно перебирать результаты вместо того, чтобы иметь их все сразу.
Некоторые тесты на моей машине:
Тестовый код:
let test f N =
let fss0 = List.init N (fun i -> List.init (i+1) (fun j -> j+i*i+i))
f fss0 |> Seq.length
Результаты в FSI:
> test projection 10;;
Real: 00:00:18.066, CPU: 00:00:18.062, GC gen0: 168, gen1: 157, gen2: 7
val it : int = 3628800
> test cartesian 10;;
Real: 00:00:19.822, CPU: 00:00:19.828, GC gen0: 244, gen1: 121, gen2: 3
val it : int = 3628800
> test cartesian2 10;;
Real: 00:00:09.247, CPU: 00:00:09.250, GC gen0: 94, gen1: 52, gen2: 2
val it : int = 3628800
> test cartesian3 10;;
Real: 00:00:04.254, CPU: 00:00:04.250, GC gen0: 359, gen1: 1, gen2: 0
val it : int = 3628800
Ответ 2
Эта функция - Haskell sequence (хотя sequence
более общий). Перевод на F #:
let sequence lss =
let k l ls = [ for x in l do for xs in ls -> x::xs ]
List.foldBack k lss [[]]
в интерактивном режиме:
> test projection 10;;
Real: 00:00:12.240, CPU: 00:00:12.807, GC gen0: 163, gen1: 155, gen2: 4
val it : int = 3628800
> test sequence 10;;
Real: 00:00:06.038, CPU: 00:00:06.021, GC gen0: 75, gen1: 74, gen2: 0
val it : int = 3628800
Общая идея: избегать явной рекурсии в пользу стандартных комбинаторов (fold, map и т.д.)
Ответ 3
Вы выполняете медленную работу из-за операции @(i.e List concat), которая является медленной операцией и выполняется много раз рекурсивным образом. Причиной того, что @является медленным, является то, что List является связанным списком в функциональном программировании и в список concat 2, который вы должны сначала пройти до конца списка (один за другим, проходящий через элементы), а затем добавить другой список.
Пожалуйста, просмотрите предлагаемые ссылки в комментариях. Я надеюсь, что это поможет вам.
Ответ 4
Здесь рекурсивная версия. Это не так быстро, как некоторые из других решений (только на 25% быстрее, чем ваша оригинальная функция), но использование памяти является постоянным, поэтому оно работает для чрезвычайно больших наборов результатов.
let cartesian l =
let rec aux f = function
| [] -> f (Seq.singleton [])
| h::t -> aux (fun acc -> f (Seq.collect (fun x -> (Seq.map (fun y -> y::x) h)) acc)) t
aux id l
Ответ 5
let crossProduct listA listB listC listD listE =
listA |> Seq.collect (fun a ->
listB |> Seq.collect (fun b ->
listC |> Seq.collect (fun c ->
listD |> Seq.collect (fun d ->
listE |> Seq.map (fun e -> a,b,c,d,e))