Комбинации и перестановки в F #

Недавно я написал следующие комбинации и функции перестановок для проекта F #, но я вполне уверен, что они далеки от оптимизации.

/// Rotates a list by one place forward.
let rotate lst =
    List.tail lst @ [List.head lst]

/// Gets all rotations of a list.
let getRotations lst =
    let rec getAll lst i = if i = 0 then [] else lst :: (getAll (rotate lst) (i - 1))
    getAll lst (List.length lst)

/// Gets all permutations (without repetition) of specified length from a list.
let rec getPerms n lst = 
    match n, lst with
    | 0, _ -> seq [[]]
    | _, [] -> seq []
    | k, _ -> lst |> getRotations |> Seq.collect (fun r -> Seq.map ((@) [List.head r]) (getPerms (k - 1) (List.tail r)))

/// Gets all permutations (with repetition) of specified length from a list.
let rec getPermsWithRep n lst = 
    match n, lst with
    | 0, _ -> seq [[]]
    | _, [] -> seq []
    | k, _ -> lst |> Seq.collect (fun x -> Seq.map ((@) [x]) (getPermsWithRep (k - 1) lst))
    // equivalent: | k, _ -> lst |> getRotations |> Seq.collect (fun r -> List.map ((@) [List.head r]) (getPermsWithRep (k - 1) r))

/// Gets all combinations (without repetition) of specified length from a list.
let rec getCombs n lst = 
    match n, lst with
    | 0, _ -> seq [[]]
    | _, [] -> seq []
    | k, (x :: xs) -> Seq.append (Seq.map ((@) [x]) (getCombs (k - 1) xs)) (getCombs k xs)

/// Gets all combinations (with repetition) of specified length from a list.
let rec getCombsWithRep n lst = 
    match n, lst with
    | 0, _ -> seq [[]]
    | _, [] -> seq []
    | k, (x :: xs) -> Seq.append (Seq.map ((@) [x]) (getCombsWithRep (k - 1) lst)) (getCombsWithRep k xs)

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

Update

Здесь моя новая реализация для функции getPerms, вдохновленная Томасом.

К сожалению, на самом деле это не так быстро, как существующая. Предложения?

let getPerms n lst =
    let rec getPermsImpl acc n lst = seq {
        match n, lst with
        | k, x :: xs ->
            if k > 0 then
                for r in getRotations lst do
                    yield! getPermsImpl (List.head r :: acc) (k - 1) (List.tail r)
            if k >= 0 then yield! getPermsImpl acc k []
        | 0, [] -> yield acc
        | _, [] -> ()
        }
    getPermsImpl List.empty n lst

Ответ 1

Я заметил, что ваша обновленная функция getPerms содержит дубликаты. Вот моя трещина в бесплатной версии. Надеемся, что комментарии говорят сами за себя. Самая сложная часть заключалась в написании эффективной функции distrib, потому что оператор конкатенации должен использоваться где-то. К счастью, он используется только на небольших подсписок, поэтому производительность остается разумной. Мой код getAllPerms ниже генерирует все перестановки в [1..9] примерно за четверть секунды, все 10-элементные перестановки примерно за 2,5 секунды.

Изменить: смешно, я не смотрел код Томаса, но его функции сочетания и моя функция выбора почти идентичны.

// All ordered picks {x_i1, x_i2, .. , x_ik} of k out of n elements {x_1,..,x_n}
// where i1 < i2 < .. < ik
let picks n L = 
    let rec aux nleft acc L = seq {
        match nleft,L with
        | 0,_ -> yield acc
        | _,[] -> ()
        | nleft,h::t -> yield! aux (nleft-1) (h::acc) t
                        yield! aux nleft acc t }
    aux n [] L

// Distribute an element y over a list:
// {x1,..,xn} --> {y,x1,..,xn}, {x1,y,x2,..,xn}, .. , {x1,..,xn,y}
let distrib y L =
    let rec aux pre post = seq {
        match post with
        | [] -> yield (L @ [y])
        | h::t -> yield (pre @ y::post)
                  yield! aux (pre @ [h]) t }
    aux [] L

// All permutations of a single list = the head of a list distributed
// over all permutations of its tail
let rec getAllPerms = function
    | [] -> Seq.singleton []
    | h::t -> getAllPerms t |> Seq.collect (distrib h)

// All k-element permutations out of n elements = 
// all permutations of all ordered picks of length k combined
let getPerms2 n lst = picks n lst |> Seq.collect getAllPerms

Изменить: больше кода в ответ на комментарии

// Generates the cartesian outer product of a list of sequences LL
let rec outerProduct = function
    | [] -> Seq.singleton []
    | L::Ls -> L |> Seq.collect (fun x -> 
                outerProduct Ls |> Seq.map (fun L -> x::L))

// Generates all n-element combination from a list L
let getPermsWithRep2 n L = 
    List.replicate n L |> outerProduct  

Ответ 2

Если вы хотите написать эффективный функциональный код, рекомендуется избегать использования оператора @, потому что конкатенация списков очень неэффективна.

Вот пример того, как вы можете написать функцию для создания всех комбинаций:

let rec combinations acc size set = seq {
  match size, set with 
  | n, x::xs -> 
      if n > 0 then yield! combinations (x::acc) (n - 1) xs
      if n >= 0 then yield! combinations acc n xs 
  | 0, [] -> yield acc 
  | _, [] -> () }

combinations [] 3 [1 .. 4]

Параметры функции:

  • acc используется для запоминания элементов, которые уже выбраны для включения в комбинацию (изначально это пустой список)
  • size - это оставшееся количество элементов, которые нам нужно добавить в acc (изначально это необходимый размер комбинаций)
  • set - это набор элементов, которые можно выбрать из

Функция реализована с использованием простой рекурсии. Если нам нужно создать комбинации размера n, то мы можем либо добавить, либо не добавлять текущий элемент, поэтому мы пытаемся сгенерировать комбинации с использованием обоих опций (первый случай) и добавить их все в сгенерированную последовательность, используя yield!. Если нам нужно еще 0 элементов, то мы успешно сгенерировали комбинацию (второй случай), и если мы закончим с каким-то другим числом, но не будем использовать какие-либо оставшиеся элементы, мы не сможем вернуть ничего (последний случай).

Комбинации с повторением будут похожими - разница в том, что вам не нужно удалять элементы из списка (используя только xs в рекурсивных вызовах), поэтому есть больше вариантов того, что делать.

Ответ 3

Если у вас есть настоящая потребность в скорости, я рекомендую вам сначала найти самый быстрый алгоритм для вашей проблемы, и если алгоритм окажется неотъемлемо императивным (например, пузырьковая сортировка или сито из Eratosthenes), во что бы то ни стало, используйте F # обязательные функции для вашей реализации, сохраняя ваш API для пользователей библиотеки (больше работы и риска для вас, но отличные результаты для пользователей библиотеки).

В зависимости от вашего вопроса я адаптировал свою быструю реализацию для генерации всех перестановок набора лексикографически (изначально представленного здесь) для генерации перестановок r-длины:

open System
open System.Collections.Generic

let flip f x y = f y x

///Convert the given function to an IComparer<'a>
let comparer f = { new IComparer<_> with member self.Compare(x,y) = f x y }

///generate r-length lexicographical permutations of e using the comparison function f.
///permutations start with e and continue until the last lexicographical permutation of e:
///if you want all permuations for a given set, make sure to order e before callings this function.
let lexPerms f r e =
    if r < 0 || r > (Seq.length e) then
        invalidArg "e" "out of bounds" |> raise

    //only need to compute IComparers used for Array.Sort in-place sub-range overload once
    let fComparer = f |> comparer
    let revfComparer = f |> flip |> comparer

    ///Advances (mutating) perm to the next lexical permutation.
    let lexPermute perm =
        //sort last perm.Length - r elements in decreasing order,
        //thereby avoiding duplicate permutations of the first r elements
        //todo: experiment with eliminate this trick and instead concat all
        //lex perms generated from ordered combinations of length r of e (like cfern)
        Array.Sort(perm, r, Array.length perm - r, revfComparer)

        //Find the index, call it s, just before the longest "tail" that is
        //ordered  in decreasing order ((s+1)..perm.Length-1).
        let rec tryFind i =
            if i = 0 then
                None
            elif (f perm.[i] perm.[i-1]) >= 0 then
                Some(i-1)
            else
                tryFind (i-1)

        match tryFind (perm.Length-1) with
        | Some s ->
            let sValue = perm.[s]

            //Change the value just before the tail (sValue) to the
            //smallest number bigger than it in the tail (perm.[t]).
            let rec find i imin =
                if i = perm.Length then
                    imin
                elif (f perm.[i] sValue) > 0 && (f perm.[i] perm.[imin]) < 0 then
                    find (i+1) i
                else
                    find (i+1) imin

            let t = find (s+1) (s+1)

            perm.[s] <- perm.[t]
            perm.[t] <- sValue

            //Sort the tail in increasing order.
            Array.Sort(perm, s+1, perm.Length - s - 1, fComparer)
            true
        | None ->
            false

    //yield copies of each perm
    seq {
        let e' = Seq.toArray e
        yield e'.[..r-1]
        while lexPermute e' do
            yield e'.[..r-1]
    }

let lexPermsAsc r e = lexPerms compare r e
let lexPermsDesc r e = lexPerms (flip compare) r e

Я не уверен, что адаптация этого алгоритма к перестановкам r-длины ужасно неуместна (т.е. есть ли лучшие императивные или функциональные алгоритмы специально для этой проблемы), но в среднем он выполняет почти в два раза быстрее, чем ваш последний getPerms для набора [1;2;3;4;5;6;7;8;9], и имеет дополнительную особенность, позволяющую лексикографически перенести г-длины (обратите внимание также на то, что lexPermsAsc не монотонна как функция от r):

r       lexPermsAsc(s)  getPerms(s)
1       0.002           0.002
2       0.004           0.002
3       0.019           0.007
4       0.064           0.014
5       0.264           0.05
6       0.595           0.307
7       1.276           0.8
8       1.116           2.247
9       1.107           4.235
avg.:   0.494           0.852