在OCaml中编写置换函数的理想方法

时间:2013-11-25 14:56:00

标签: algorithm functional-programming ocaml permutation

给出一个列表,并输出所有排列的列表。

这是我的想法:

递归地,hd::tl的排列是将hd分发到tl的每个排列列表中。通过分发hd,我的意思是将hd插入列表中的每个可能的插槽中。

例如,将1分发到[2;3]会生成[[1;2;3];[2;1;3];[2;3;1]]

所以这是代码

let distribute c l =
  let rec insert acc1 acc2 = function
    | [] -> acc2
    | hd::tl ->
      insert (hd::acc1) ((List.rev_append acc1 (hd::c::tl)) :: acc2) tl
  in 
  insert [] [c::l] l

let rec permutation = function
  | [] -> [[]]
  | hd::tl -> 
    List.fold_left (fun acc x -> List.rev_append (distribute hd x) acc) [] (permutation tl)

我猜上面的代码是正确的。

我的问题是:在OCaml中编写排列是否有更好的方法(在简单性,性能,空间效率等方面)?

3 个答案:

答案 0 :(得分:5)

其他方法包括a)对于每个元素,将其添加到列表中除元素之外的每个排列,或者b)从每个元素生成排列!指数。请参阅Rosetta Code示例。我怀疑它们具有相似的空间复杂性。

答案 1 :(得分:1)

这是一种生成排列的流(即,惰性列表)的解决方案。这避免了必须立即将整个列表保存在内存中。如果您只需要有限数量的排列,它可以让您使用更长的列表。

为了简单起见,我使用Jane Street Core作为我的基础库。

open Core.Std

let permutations lst =
    let lstar = Array.of_list lst in
    let len = Array.length lstar in
    let ks = List.range 1 (len + 1) in
    let indices = Int.Set.of_list (List.range 0 len) in
    let choose k (v, indices, res) =
        let ix = Option.value_exn (Int.Set.find_index indices (v mod k)) in
        (v / k, Int.Set.remove indices ix, lstar.(ix) :: res)
    in
    let perm i =
        let (v, _, res) =
            List.fold_right ks ~f: choose ~init: (i, indices, [])
        in
        if v > 0 then None else Some res
    in
    Stream.from perm

以下是此实施的会话:

# let s = permutations ['a'; 'b'; 'c'; 'd'];;
val s : char list Stream.t = <abstr>
# Stream.npeek 100 s;;
- : char list list =
[[d; c; b; a]; [d; c; a; b]; [d; b; a; c]; [c; b; a; d]; [d; b; c; a];
 [d; a; c; b]; [d; a; b; c]; [c; a; b; d]; [c; b; d; a]; [c; a; d; b];
 [b; a; d; c]; [b; a; c; d]; [c; d; b; a]; [c; d; a; b]; [b; d; a; c];
 [b; c; a; d]; [b; d; c; a]; [a; d; c; b]; [a; d; b; c]; [a; c; b; d];
 [b; c; d; a]; [a; c; d; b]; [a; b; d; c]; [a; b; c; d]]
# let s = permutations (List.range 0 10);;
val s : int list Stream.t = <abstr>
# Stream.iter ignore s;;
- : unit = ()
# Stream.count s;;
- : int = 3628800
# fact 10;;
- : int = 3628800

关键的想法是,您可以通过一种方式对排列进行编号,从而可以轻松地从索引中提取排列本身。直觉类似于对整数的十进制表示进行编号的方式。要从索引中提取4位数字的十进制表示,您可以执行以下操作:

let digits i =
    let ks = [10;10;10;10] in
    let extract k (v, result) = (v / k, v mod k :: result) in
    let (_, res) = List.fold_right ks ~f: extract ~init: (i, []) in
    res

请注意,这每次都使用10作为除数。要提取排列,除了使用n,n - 1,n - 2,...,1作为除数之外,你会做一些非常相似的事情。

答案 2 :(得分:0)

你所拥有的基本上是一种“基于插入”的方法 - 你按顺序浏览输入列表,然后以各种可能的方式将每个后续输入元素“插入”到部分结果中。

您可以采用的另一种方法是“基于选择”的方法 - 您可以通过从未使用的值中“选择”每种可能性来构建结果列表的每个后续元素。形式上,置换具有来自输入列表的任何元素的第一元素,并且第一元素之后的置换部分来自输入列表的“其余”的任何排列。这会生成所有排列。

棘手的部分是如何获得“列表的其余部分”。最简单的方法是编写一个“删除”函数,它接受一个值和一个列表,并返回列表的副本,而第一个元素不等于给定的元素。但是,对于不具有所有类型相等性的语言,或者您希望区分不同但相同的元素的语言,这会遇到问题。更正确的方法是在迭代列表时同时生成元素和“列表的其余部分”。