【问题标题】:The ideal way to write permutation function in OCaml在 OCaml 中编写置换函数的理想方法
【发布时间】:2013-11-25 14:56:00
【问题描述】:

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

这是我的想法:

递归地,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 中编写排列?

【问题讨论】:

  • 不是 [1;2;3] 的所有排列都是 [1;2;3],[1;3;2],[2;1;3],[2;3 ;1],[3;1;2],[3;2;1] ?
  • @aneccodeal 是的。请注意我给出的示例仅适用于distribute,不适用于permutation
  • 如果您关心性能,您可以直接为递归底部附近的小列表编写结果。如果您想要中等大小的列表的排列,您可以将结果重新编码为流而不是列表。 (实际上,您无法获得列表的所有排列长度超过 13 或 14。)
  • @JeffreyScofield 你在谈论惰性列表吗?如果是这样,我认为这是一个很好的观点。你能从中得出一个答案吗?
  • 是的,惰性列表。我认为,懒惰地生成排列是一个挑战。我看看能不能解决。

标签: algorithm functional-programming ocaml permutation


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

    为了简单起见,我使用 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 作为除数。

    【讨论】:

      【解决方案3】:

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-10-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-01-20
        • 1970-01-01
        • 1970-01-01
        • 2015-07-22
        相关资源
        最近更新 更多