练习类别


列表 列表尾部 列表最后两个元素 列表的第 N 个元素 列表长度 反转列表 回文 展开列表 消除重复项 压缩连续重复项 运行长度编码 修改后的运行长度编码 解码运行长度编码列表 列表的运行长度编码(直接解法) 复制列表元素 复制列表元素特定次数 从列表中删除每隔 N 个元素 将列表拆分为两部分;第一部分的长度已知 从列表中提取切片 将列表向左旋转 N 个位置 从列表中删除第 K 个元素 在列表的给定位置插入元素 创建包含给定范围内所有整数的列表 从列表中提取给定数量的随机元素 彩票:从集合 1..M 中抽取 N 个不同的随机数 生成列表元素的随机排列 生成从列表 N 个元素中选取 K 个不同对象的组合 将集合的元素分组为不相交的子集 根据子列表长度对列表列表进行排序

算术 判断给定整数是否为素数 确定两个正整数的最大公约数 确定两个正整数是否互素 计算欧拉函数 Φ(m) 确定给定正整数的素因子 确定给定正整数的素因子 (2) 计算欧拉函数 Φ(m) (改进) 比较两种计算欧拉函数的方法 素数列表 哥德巴赫猜想 哥德巴赫分解列表

逻辑与代码 逻辑表达式真值表 (2个变量) 逻辑表达式真值表 格雷码 霍夫曼编码

二叉树 构造完全平衡二叉树 对称二叉树 二叉搜索树 (字典) 生成与测试范式 构造高度平衡二叉树 用给定节点数构造高度平衡二叉树 统计二叉树的叶子节点 将二叉树的叶子节点收集到列表中 将二叉树的内部节点收集到列表中 将给定层级的节点收集到列表中 构造完全二叉树 布置二叉树 (1) 布置二叉树 (2) 布置二叉树 (3) 二叉树的字符串表示 二叉树的前序遍历和中序遍历序列 二叉树的点串表示

多叉树 从节点字符串构建树 计算多叉树的节点数 确定树的内部路径长度 构建树节点自下而上的顺序序列 Lisp 风格的树表示

转换 从一个节点到另一个节点的路径 从给定节点开始的循环 构建所有生成树 构建最小生成树 图同构 节点度数和图着色 深度优先遍历图 连通分量 二分图 生成具有 N 个节点的 K 规则简单图

其他 八皇后问题 骑士巡游 冯·科赫猜想 一个算术谜题 英文数字单词 语法检查器 数独 非数字游戏 填字游戏

练习

本节内容灵感来源于 九十九个 Lisp 问题,而该问题又基于 Werner Hett 的 “ Prolog 问题列表”。对于每个问题,都展示了一些简单的测试用例,如果需要,这些测试用例也可以帮助阐明问题。要着手解决这些问题,我们建议您先 安装 OCaml 或在 浏览器中使用 它。以下问题的源代码可以在 GitHub 上找到。

每个练习都包含一个难度等级,从初学者到高级。

列表的尾部

初级

编写一个函数 last : 'a list -> 'a option,该函数返回列表的最后一个元素。

# last ["a" ; "b" ; "c" ; "d"];;
- : string option = Some "d"
# last [];;
- : 'a option = None
# let rec last = function 
  | [] -> None
  | [ x ] -> Some x
  | _ :: t -> last t;;
val last : 'a list -> 'a option = <fun>

列表的最后两个元素

初级

查找列表的最后两个元素(最后一个元素和倒数第二个元素)。

# last_two ["a"; "b"; "c"; "d"];;
- : (string * string) option = Some ("c", "d")
# last_two ["a"];;
- : (string * string) option = None
# let rec last_two = function
    | [] | [_] -> None
    | [x; y] -> Some (x,y)
    | _ :: t -> last_two t;;
val last_two : 'a list -> ('a * 'a) option = <fun>

列表的第 N 个元素

初级

查找列表的第 N 个元素。

注意: OCaml 有 List.nth 函数,它从 0 开始对元素进行编号,如果索引超出范围,则会引发异常。

# List.nth ["a"; "b"; "c"; "d"; "e"] 2;;
- : string = "c"
# List.nth ["a"] 2;;
Exception: Failure "nth".
# let rec at k = function
    | [] -> None
    | h :: t -> if k = 0 then Some h else at (k - 1) t;;
val at : int -> 'a list -> 'a option = <fun>

列表的长度

初级

查找列表的元素数量。

OCaml 标准库有 List.length 函数,但我们要求您重新实现它。如果能实现 尾递归 解决方案,将获得奖励。

# length ["a"; "b"; "c"];;
- : int = 3
# length [];;
- : int = 0

此函数是尾递归的:它使用恒定的堆栈内存量,而与列表的大小无关。

# let length list =
    let rec aux n = function
      | [] -> n
      | _ :: t -> aux (n + 1) t
    in
    aux 0 list;;
val length : 'a list -> int = <fun>

反转列表

初级

反转一个列表。

OCaml 标准库有 List.rev 函数,但我们要求您重新实现它。

# rev ["a"; "b"; "c"];;
- : string list = ["c"; "b"; "a"]
# let rev list =
    let rec aux acc = function
      | [] -> acc
      | h :: t -> aux (h :: acc) t
    in
    aux [] list;;
val rev : 'a list -> 'a list = <fun>

回文

初级

确定一个列表是否为回文。

提示: 回文是其自身的逆序。

# is_palindrome ["x"; "a"; "m"; "a"; "x"];;
- : bool = true
# not (is_palindrome ["a"; "b"]);;
- : bool = true
# let is_palindrome list =
    (* One can use either the rev function from the previous problem, or the built-in List.rev *)
    list = List.rev list;;
val is_palindrome : 'a list -> bool = <fun>

扁平化列表

中级

扁平化嵌套列表结构。

type 'a node =
  | One of 'a 
  | Many of 'a node list
# flatten [One "a"; Many [One "b"; Many [One "c" ;One "d"]; One "e"]];;
- : string list = ["a"; "b"; "c"; "d"; "e"]
# type 'a node =
    | One of 'a 
    | Many of 'a node list;;
type 'a node = One of 'a | Many of 'a node list
# (* This function traverses the list, prepending any encountered elements
    to an accumulator, which flattens the list in inverse order. It can
    then be reversed to obtain the actual flattened list. *);;
# let flatten list =
    let rec aux acc = function
      | [] -> acc
      | One x :: t -> aux (x :: acc) t
      | Many l :: t -> aux (aux acc l) t
    in
    List.rev (aux [] list);;
val flatten : 'a node list -> 'a list = <fun>

消除重复

中级

消除列表元素中连续的重复项。

# compress ["a"; "a"; "a"; "a"; "b"; "c"; "c"; "a"; "a"; "d"; "e"; "e"; "e"; "e"];;
- : string list = ["a"; "b"; "c"; "a"; "d"; "e"]
# let rec compress = function
    | a :: (b :: _ as t) -> if a = b then compress t else a :: compress t
    | smaller -> smaller;;
val compress : 'a list -> 'a list = <fun>

打包连续重复项

中级

将列表元素中连续的重复项打包到子列表中。

# pack ["a"; "a"; "a"; "a"; "b"; "c"; "c"; "a"; "a"; "d"; "d"; "e"; "e"; "e"; "e"];;
- : string list list =
[["a"; "a"; "a"; "a"]; ["b"]; ["c"; "c"]; ["a"; "a"]; ["d"; "d"];
 ["e"; "e"; "e"; "e"]]
# let pack list =
    let rec aux current acc = function
      | [] -> []    (* Can only be reached if original list is empty *)
      | [x] -> (x :: current) :: acc
      | a :: (b :: _ as t) ->
         if a = b then aux (a :: current) acc t
         else aux [] ((a :: current) :: acc) t  in
    List.rev (aux [] [] list);;
val pack : 'a list -> 'a list list = <fun>

行程长度编码

初级

如果您需要的话,请复习一下 行程长度编码

以下是一个示例

# encode ["a"; "a"; "a"; "a"; "b"; "c"; "c"; "a"; "a"; "d"; "e"; "e"; "e"; "e"];;
- : (int * string) list =
[(4, "a"); (1, "b"); (2, "c"); (2, "a"); (1, "d"); (4, "e")]
# let encode list =
    let rec aux count acc = function
      | [] -> [] (* Can only be reached if original list is empty *)
      | [x] -> (count + 1, x) :: acc
      | a :: (b :: _ as t) -> if a = b then aux (count + 1) acc t
                              else aux 0 ((count + 1, a) :: acc) t in
    List.rev (aux 0 [] list);;
val encode : 'a list -> (int * 'a) list = <fun>

另一种解决方案更短,但需要更多的内存,它使用在问题 9 中定义的 pack 函数。

# let pack list =
    let rec aux current acc = function
      | [] -> []    (* Can only be reached if original list is empty *)
      | [x] -> (x :: current) :: acc
      | a :: (b :: _ as t) ->
         if a = b then aux (a :: current) acc t
         else aux [] ((a :: current) :: acc) t  in
    List.rev (aux [] [] list);;
val pack : 'a list -> 'a list list = <fun>
# let encode list =
    List.map (fun l -> (List.length l, List.hd l)) (pack list);;
val encode : 'a list -> (int * 'a) list = <fun>

修改后的行程长度编码

初级

修改上一个问题的结果,以使其在元素没有重复项时,直接将其复制到结果列表中。只有具有重复项的元素才会以 (N E) 列表的形式进行转移。

由于 OCaml 列表是同质的,因此需要定义一个类型来容纳单个元素和子列表。

type 'a rle =
  | One of 'a
  | Many of int * 'a
# encode ["a"; "a"; "a"; "a"; "b"; "c"; "c"; "a"; "a"; "d"; "e"; "e"; "e"; "e"];;
- : string rle list =
[Many (4, "a"); One "b"; Many (2, "c"); Many (2, "a"); One "d";
 Many (4, "e")]
# type 'a rle =
  | One of 'a
  | Many of int * 'a;;
type 'a rle = One of 'a | Many of int * 'a
# let encode l =
    let create_tuple cnt elem =
      if cnt = 1 then One elem
      else Many (cnt, elem) in
    let rec aux count acc = function
      | [] -> []
      | [x] -> (create_tuple (count + 1) x) :: acc
      | hd :: (snd :: _ as tl) ->
          if hd = snd then aux (count + 1) acc tl
          else aux 0 ((create_tuple (count + 1) hd) :: acc) tl in
      List.rev (aux 0 [] l);;
val encode : 'a list -> 'a rle list = <fun>

解码行程长度编码列表

中级

给定一个按照先前问题中规定的方式生成的行程长度编码列表,构造其未压缩版本。

#  decode [Many (4, "a"); One "b"; Many (2, "c"); Many (2, "a"); One "d"; Many (4, "e")];;
- : string list =
["a"; "a"; "a"; "a"; "b"; "c"; "c"; "a"; "a"; "d"; "e"; "e"; "e"; "e"]
# let decode list =
    let rec many acc n x =
      if n = 0 then acc else many (x :: acc) (n - 1) x
    in
    let rec aux acc = function
      | [] -> acc
      | One x :: t -> aux (x :: acc) t
      | Many (n, x) :: t -> aux (many acc n x) t
    in
      aux [] (List.rev list);;
val decode : 'a rle list -> 'a list = <fun>

列表的行程长度编码(直接解法)

中级

直接实现所谓的行程长度编码数据压缩方法。即,不要像问题 "将列表元素的连续重复项打包到子列表中" 中那样显式地创建包含重复项的子列表,而只对它们进行计数。与问题 "修改后的行程长度编码" 中一样,通过将单例列表 (1 X) 替换为 X 来简化结果列表。

# encode ["a";"a";"a";"a";"b";"c";"c";"a";"a";"d";"e";"e";"e";"e"];;
- : string rle list =
[Many (4, "a"); One "b"; Many (2, "c"); Many (2, "a"); One "d";
 Many (4, "e")]
# let encode list =
    let rle count x = if count = 0 then One x else Many (count + 1, x) in
    let rec aux count acc = function
      | [] -> [] (* Can only be reached if original list is empty *)
      | [x] -> rle count x :: acc
      | a :: (b :: _ as t) -> if a = b then aux (count + 1) acc t
                              else aux 0 (rle count a :: acc) t
    in
      List.rev (aux 0 [] list);;
val encode : 'a list -> 'a rle list = <fun>

重复列表的元素

初级

重复列表的元素。

# duplicate ["a"; "b"; "c"; "c"; "d"];;
- : string list = ["a"; "a"; "b"; "b"; "c"; "c"; "c"; "c"; "d"; "d"]
# let rec duplicate = function
    | [] -> []
    | h :: t -> h :: h :: duplicate t;;
val duplicate : 'a list -> 'a list = <fun>

备注:此函数不是尾递归的。你能修改它使其成为尾递归吗?

将列表的元素复制指定次数

中级

将列表的元素复制指定次数。

# replicate ["a"; "b"; "c"] 3;;
- : string list = ["a"; "a"; "a"; "b"; "b"; "b"; "c"; "c"; "c"]
# let replicate list n =
    let rec prepend n acc x =
      if n = 0 then acc else prepend (n-1) (x :: acc) x in
    let rec aux acc = function
      | [] -> acc
      | h :: t -> aux (prepend n acc h) t in
    (* This could also be written as:
       List.fold_left (prepend n) [] (List.rev list) *)
    aux [] (List.rev list);;
val replicate : 'a list -> int -> 'a list = <fun>

请注意,List.rev list 仅是因为我们希望 aux尾递归 的。

从列表中删除每个第 N 个元素

中级

从列表中删除每个第 N 个元素。

# drop ["a"; "b"; "c"; "d"; "e"; "f"; "g"; "h"; "i"; "j"] 3;;
- : string list = ["a"; "b"; "d"; "e"; "g"; "h"; "j"]
# let drop list n =
    let rec aux i = function
      | [] -> []
      | h :: t -> if i = n then aux 1 t else h :: aux (i + 1) t  in
    aux 1 list;;
val drop : 'a list -> int -> 'a list = <fun>

将列表拆分为两个部分;第一部分的长度已知

初级

将列表拆分为两个部分;第一部分的长度已知。

如果第一部分的长度大于整个列表,则第一部分为列表,第二部分为空。

# split ["a"; "b"; "c"; "d"; "e"; "f"; "g"; "h"; "i"; "j"] 3;;
- : string list * string list =
(["a"; "b"; "c"], ["d"; "e"; "f"; "g"; "h"; "i"; "j"])
# split ["a"; "b"; "c"; "d"] 5;;
- : string list * string list = (["a"; "b"; "c"; "d"], [])
# let split list n =
    let rec aux i acc = function
      | [] -> List.rev acc, []
      | h :: t as l -> if i = 0 then List.rev acc, l
                       else aux (i - 1) (h :: acc) t 
    in
      aux n [] list;;
val split : 'a list -> int -> 'a list * 'a list = <fun>

从列表中提取片段

中级

给定两个索引 ik,片段是包含原始列表中第 i 个和第 k 个元素之间元素的列表(包括两个边界)。从 0 开始计数元素(这是 List 模块对元素进行编号的方式)。

# slice ["a"; "b"; "c"; "d"; "e"; "f"; "g"; "h"; "i"; "j"] 2 6;;
- : string list = ["c"; "d"; "e"; "f"; "g"]
# let slice list i k =
    let rec take n = function
      | [] -> []
      | h :: t -> if n = 0 then [] else h :: take (n - 1) t
    in
    let rec drop n = function
      | [] -> []
      | h :: t as l -> if n = 0 then l else drop (n - 1) t
    in
    take (k - i + 1) (drop i list);;
val slice : 'a list -> int -> int -> 'a list = <fun>

此解决方案有一个缺点,即 take 函数不是 尾递归 的,因此当给定非常长的列表时可能会耗尽堆栈。你可能还会注意到 takedrop 的结构相似,你可能希望在一个函数中抽象出它们共同的骨架。以下是一个解决方案。

# let rec fold_until f acc n = function
    | [] -> (acc, [])
    | h :: t as l -> if n = 0 then (acc, l)
                     else fold_until f (f acc h) (n - 1) t
  let slice list i k =
    let _, list = fold_until (fun _ _ -> []) [] i list in
    let taken, _ = fold_until (fun acc h -> h :: acc) [] (k - i + 1) list in
    List.rev taken;;
val fold_until : ('a -> 'b -> 'a) -> 'a -> int -> 'b list -> 'a * 'b list =
  <fun>
val slice : 'a list -> int -> int -> 'a list = <fun>

将列表向左旋转 N 个位置

中级

将列表向左旋转 N 个位置。

# rotate ["a"; "b"; "c"; "d"; "e"; "f"; "g"; "h"] 3;;
- : string list = ["d"; "e"; "f"; "g"; "h"; "a"; "b"; "c"]
# let split list n =
    let rec aux i acc = function
      | [] -> List.rev acc, []
      | h :: t as l -> if i = 0 then List.rev acc, l
                       else aux (i - 1) (h :: acc) t  in
    aux n [] list

  let rotate list n =
    let len = List.length list in
    (* Compute a rotation value between 0 and len - 1 *)
    let n = if len = 0 then 0 else (n mod len + len) mod len in
    if n = 0 then list
    else let a, b = split list n in b @ a;;
val split : 'a list -> int -> 'a list * 'a list = <fun>
val rotate : 'a list -> int -> 'a list = <fun>

从列表中删除第 K 个元素

初级

从列表中删除第 K 个元素。

列表的第一个元素编号为 0,第二个编号为 1,...

# remove_at 1 ["a"; "b"; "c"; "d"];;
- : string list = ["a"; "c"; "d"]
# let rec remove_at n = function
    | [] -> []
    | h :: t -> if n = 0 then t else h :: remove_at (n - 1) t;;
val remove_at : int -> 'a list -> 'a list = <fun>

在列表的给定位置插入元素

初级

从 0 开始计数列表元素。如果位置大于或等于列表的长度,则将元素插入末尾。(如果位置为负,则行为未定义。)

# insert_at "alfa" 1 ["a"; "b"; "c"; "d"];;
- : string list = ["a"; "alfa"; "b"; "c"; "d"]
# let rec insert_at x n = function
    | [] -> [x]
    | h :: t as l -> if n = 0 then x :: l else h :: insert_at x (n - 1) t;;
val insert_at : 'a -> int -> 'a list -> 'a list = <fun>

创建一个包含给定范围内所有整数的列表

初级

如果第一个参数大于第二个参数,则以递减顺序生成列表。

# range 4 9;;
- : int list = [4; 5; 6; 7; 8; 9]
# let range a b =
    let rec aux a b =
      if a > b then [] else a :: aux (a + 1) b
    in
      if a > b then List.rev (aux b a) else aux a b;;
val range : int -> int -> int list = <fun>

尾递归实现

# let range a b =
    let rec aux acc high low =
      if high >= low then
        aux (high :: acc) (high - 1) low
      else acc
    in
      if a < b then aux [] b a else List.rev (aux [] a b);;
val range : int -> int -> int list = <fun>

从列表中提取指定数量的随机选择的元素

中级

所选项目将返回到一个列表中。我们使用 Random 模块,并在函数开头使用 Random.init 0 初始化它以确保可重复性并验证解决方案。但是,为了使该函数真正随机,应该删除对 Random.init 0 的调用。

# rand_select ["a"; "b"; "c"; "d"; "e"; "f"; "g"; "h"] 3;;
- : string list = ["e"; "c"; "g"]
# let rand_select list n =
    Random.init 0;
    let rec extract acc n = function
      | [] -> raise Not_found
      | h :: t -> if n = 0 then (h, acc @ t) else extract (h :: acc) (n - 1) t
    in
    let extract_rand list len =
      extract [] (Random.int len) list
    in
    let rec aux n acc list len =
      if n = 0 then acc else
        let picked, rest = extract_rand list len in
        aux (n - 1) (picked :: acc) rest (len - 1)
    in
    let len = List.length list in
      aux (min n len) [] list len;;
val rand_select : 'a list -> int -> 'a list = <fun>

彩票:从集合 1..M 中抽取 N 个不同的随机数

初级

从集合 1..M 中抽取 N 个不同的随机数。

所选数字将返回到一个列表中。

# lotto_select 6 49;;
- : int list = [20; 28; 45; 16; 24; 38]
# (* [range] and [rand_select] defined in problems above *)
  let lotto_select n m = rand_select (range 1 m) n;;
val lotto_select : int -> int -> int list = <fun>

生成列表元素的随机排列

初级

生成列表元素的随机排列。

# permutation ["a"; "b"; "c"; "d"; "e"; "f"];;
- : string list = ["c"; "d"; "f"; "e"; "b"; "a"]
# let permutation list =
    let rec extract acc n = function
      | [] -> raise Not_found
      | h :: t -> if n = 0 then (h, acc @ t) else extract (h :: acc) (n - 1) t
    in
    let extract_rand list len =
      extract [] (Random.int len) list
    in
    let rec aux acc list len =
      if len = 0 then acc else
        let picked, rest = extract_rand list len in
        aux (picked :: acc) rest (len - 1)
    in
    aux [] list (List.length list);;
val permutation : 'a list -> 'a list = <fun>

生成从列表 N 个元素中选择的 K 个不同对象的组合

中级

生成从列表 N 个元素中选择的 K 个不同对象的组合。

从 12 个人中选出 3 人的委员会,有多少种方法?我们都知道有 C(12,3) = 220 种可能性(C(N,K) 表示众所周知的二项式系数)。对于纯粹的数学家来说,这个结果可能很好。但我们希望真正地在列表中生成所有可能性。

# extract 2 ["a"; "b"; "c"; "d"];;
- : string list list =
[["a"; "b"]; ["a"; "c"]; ["a"; "d"]; ["b"; "c"]; ["b"; "d"]; ["c"; "d"]]
# let rec extract k list =
    if k <= 0 then [[]]
    else match list with
         | [] -> []
         | h :: tl ->
            let with_h = List.map (fun l -> h :: l) (extract (k - 1) tl) in
            let without_h = extract k tl in
            with_h @ without_h;;
val extract : int -> 'a list -> 'a list list = <fun>

将集合的元素分组为不相交的子集

中级

将集合的元素分组为不相交的子集

  1. 9 个人以 2、3 和 4 人的 3 个不相交小组的形式工作,有多少种方法?编写一个函数来生成所有可能性,并将它们返回到一个列表中。
  2. 以一种我们可以指定组大小列表的方式来推广上面的函数,并且该函数将返回一个组列表。
# group ["a"; "b"; "c"; "d"] [2; 1];;
- : string list list list =
[[["a"; "b"]; ["c"]]; [["a"; "c"]; ["b"]]; [["b"; "c"]; ["a"]];
 [["a"; "b"]; ["d"]]; [["a"; "c"]; ["d"]]; [["b"; "c"]; ["d"]];
 [["a"; "d"]; ["b"]]; [["b"; "d"]; ["a"]]; [["a"; "d"]; ["c"]];
 [["b"; "d"]; ["c"]]; [["c"; "d"]; ["a"]]; [["c"; "d"]; ["b"]]]
# (* This implementation is less streamlined than the one-extraction
  version, because more work is done on the lists after each
  transform to prepend the actual items. The end result is cleaner
  in terms of code, though. *)

  let group list sizes =
    let initial = List.map (fun size -> size, []) sizes in
    (* The core of the function. Prepend accepts a list of groups,
        each with the number of items that should be added, and
        prepends the item to every group that can support it, thus
        turning [1,a ; 2,b ; 0,c] into [ [0,x::a ; 2,b ; 0,c ];
        [1,a ; 1,x::b ; 0,c]; [ 1,a ; 2,b ; 0,c ]]

        Again, in the prolog language (for which these questions are
        originally intended), this function is a whole lot simpler.  *)
  let prepend p list =
    let emit l acc = l :: acc in
    let rec aux emit acc = function
      | [] -> emit [] acc
      | (n, l) as h :: t ->
         let acc = if n > 0 then emit ((n - 1, p :: l) :: t) acc
                   else acc in
         aux (fun l acc -> emit (h :: l) acc) acc t
    in
    aux emit [] list
  in
  let rec aux = function
    | [] -> [initial]
    | h :: t -> List.concat_map (prepend h) (aux t)
  in
  let all = aux list in
  (* Don't forget to eliminate all group sets that have non-full
     groups *)
  let complete = List.filter (List.for_all (fun (x, _) -> x = 0)) all in
    List.map (List.map snd) complete;;
val group : 'a list -> int list -> 'a list list list = <fun>

根据子列表的长度对列表列表进行排序

中级

根据子列表的长度对列表列表进行排序。

  1. 我们假设一个列表包含本身也是列表的元素。目标是根据它们的长度对该列表的元素进行排序。例如,短列表优先,长列表靠后,反之亦然。

  2. 同样,我们假设一个列表包含本身也是列表的元素。但这次目标是根据它们的**长度频率**对该列表的元素进行排序;也就是说,在默认情况下,当按升序排序时,长度较少的列表排在前面,长度较多的列表排在后面。

# length_sort [["a"; "b"; "c"]; ["d"; "e"]; ["f"; "g"; "h"]; ["d"; "e"];
             ["i"; "j"; "k"; "l"]; ["m"; "n"]; ["o"]];;
- : string list list =
[["o"]; ["d"; "e"]; ["d"; "e"]; ["m"; "n"]; ["a"; "b"; "c"]; ["f"; "g"; "h"];
 ["i"; "j"; "k"; "l"]]
# frequency_sort [["a"; "b"; "c"]; ["d"; "e"]; ["f"; "g"; "h"]; ["d"; "e"];
                ["i"; "j"; "k"; "l"]; ["m"; "n"]; ["o"]];;
- : string list list =
[["i"; "j"; "k"; "l"]; ["o"]; ["a"; "b"; "c"]; ["f"; "g"; "h"]; ["d"; "e"];
 ["d"; "e"]; ["m"; "n"]]
(* We might not be allowed to use built-in List.sort, so here's an
   eight-line implementation of insertion sort — O(n²) time
   complexity. *)
let rec insert cmp e = function
  | [] -> [e]
  | h :: t as l -> if cmp e h <= 0 then e :: l else h :: insert cmp e t

let rec sort cmp = function
  | [] -> []
  | h :: t -> insert cmp h (sort cmp t)

(* Sorting according to length : prepend length, sort, remove length *)
let length_sort lists =
  let lists = List.map (fun list -> List.length list, list) lists in
  let lists = sort (fun a b -> compare (fst a) (fst b)) lists in
  List.map snd lists
;;

(* Sorting according to length frequency : prepend frequency, sort,
   remove frequency. Frequencies are extracted by sorting lengths
   and applying RLE to count occurrences of each length (see problem
   "Run-length encoding of a list.") *)
let rle list =
  let rec aux count acc = function
    | [] -> [] (* Can only be reached if original list is empty *)
    | [x] -> (x, count + 1) :: acc
    | a :: (b :: _ as t) ->
       if a = b then aux (count + 1) acc t
       else aux 0 ((a, count + 1) :: acc) t in
  aux 0 [] list

let frequency_sort lists =
  let lengths = List.map List.length lists in
  let freq = rle (sort compare lengths) in
  let by_freq =
    List.map (fun list -> List.assoc (List.length list) freq , list) lists in
  let sorted = sort (fun a b -> compare (fst a) (fst b)) by_freq in
  List.map snd sorted

确定给定的整数是否为素数

中级

确定给定的整数是否为素数。

# not (is_prime 1);;
- : bool = true
# is_prime 7;;
- : bool = true
# not (is_prime 12);;
- : bool = true

回想一下,d 除以 n 当且仅当 n mod d = 0。这是一个朴素的解决方案。有关更巧妙的解决方案,请参见 埃拉托斯特尼筛法

# let is_prime n =
    let n = abs n in
    let rec is_not_divisor d =
      d * d > n || (n mod d <> 0 && is_not_divisor (d + 1)) in
    n > 1 && is_not_divisor 2;;
val is_prime : int -> bool = <fun>

确定两个正整数的最大公约数

中级

确定两个正整数的最大公约数。

使用欧几里得算法。

# gcd 13 27;;
- : int = 1
# gcd 20536 7826;;
- : int = 2
# let rec gcd a b =
    if b = 0 then a else gcd b (a mod b);;
val gcd : int -> int -> int = <fun>

确定两个正整数是否互质

初级

确定两个正整数是否互质。

如果两个数的最大公约数等于 1,则这两个数互质。

# coprime 13 27;;
- : bool = true
# not (coprime 20536 7826);;
- : bool = true
# (* [gcd] is defined in the previous question *)
  let coprime a b = gcd a b = 1;;
val coprime : int -> int -> bool = <fun>

计算欧拉的 φ(m) 函数

中级

欧拉的所谓 φ(m) 函数定义为与 m 互质的正整数 r (1 ≤ r < m) 的数量。我们令 φ(1) = 1。

如果 m 是素数,找出 φ(m) 的值。欧拉的 φ 函数在最常用的公钥密码方法(RSA)中扮演着重要角色。在本练习中,你应该使用最原始的方法来计算此函数(稍后我们将讨论更智能的方法)。

# phi 10;;
- : int = 4
# (* [coprime] is defined in the previous question *)
  let phi n =
    let rec count_coprime acc d =
      if d < n then
        count_coprime (if coprime n d then acc + 1 else acc) (d + 1)
      else acc
    in
      if n = 1 then 1 else count_coprime 0 1;;
val phi : int -> int = <fun>

确定给定正整数的素因子

中级

构造一个包含素因子的平面列表,按升序排列。

# factors 315;;
- : int list = [3; 3; 5; 7]
# (* Recall that d divides n iff [n mod d = 0] *)
  let factors n =
    let rec aux d n =
      if n = 1 then [] else
        if n mod d = 0 then d :: aux d (n / d) else aux (d + 1) n
    in
      aux 2 n;;
val factors : int -> int list = <fun>

确定给定正整数的素因子(2)

中级

构造一个包含素因子及其多重性的列表。

提示:此问题类似于问题 列表的行程长度编码(直接解法)

# factors 315;;
- : (int * int) list = [(3, 2); (5, 1); (7, 1)]
# let factors n =
    let rec aux d n =
      if n = 1 then [] else
        if n mod d = 0 then
          match aux d (n / d) with
          | (h, n) :: t when h = d -> (h, n + 1) :: t
          | l -> (d, 1) :: l
        else aux (d + 1) n
    in
      aux 2 n;;
val factors : int -> (int * int) list = <fun>

计算欧拉的 φ(m) 函数(改进版)

中级

请参见问题 "计算欧拉的 φ(m) 函数" 以了解欧拉的 φ 函数的定义。如果一个数 m 的素因子列表以之前问题中描述的形式已知,那么 φ(m) 函数可以高效地计算如下:设 [(p1, m1); (p2, m2); (p3, m3); ...] 是给定数 m 的素因子(及其多重性)列表。那么 φ(m) 可以用以下公式计算

φ(m) = (p1 - 1) × p1m1 - 1 × (p2 - 1) × p2m2 - 1 × (p3 - 1) × p3m3 - 1 × ⋯

# phi_improved 10;;
- : int = 4
# phi_improved 13;;
- : int = 12
(* Naive power function. *)
let rec pow n p = if p < 1 then 1 else n * pow n (p - 1)

(* [factors] is defined in the previous question. *)
let phi_improved n =
  let rec aux acc = function
    | [] -> acc
    | (p, m) :: t -> aux ((p - 1) * pow p (m - 1) * acc) t
  in
    aux 1 (factors n)

比较计算欧拉的 φ 函数的两种方法

初级

使用问题 "计算欧拉的 φ(m) 函数" 和 "计算欧拉的 φ(m) 函数(改进版)" 的解决方案来比较这些算法。以逻辑推理次数作为效率的度量。尝试计算 φ(10090) 作为示例。

timeit phi 10090
# (* Naive [timeit] function.  It requires the [Unix] module to be loaded. *)
  let timeit f a =
    let t0 = Unix.gettimeofday() in
      ignore (f a);
    let t1 = Unix.gettimeofday() in
      t1 -. t0;;
val timeit : ('a -> 'b) -> 'a -> float = <fun>

素数列表

初级

给定一个由下限和上限表示的整数范围,构造一个包含该范围内所有素数的列表。

# List.length (all_primes 2 7920);;
- : int = 1000
# let is_prime n =
    let n = max n (-n) in
    let rec is_not_divisor d =
      d * d > n || (n mod d <> 0 && is_not_divisor (d + 1))
    in
      is_not_divisor 2

  let rec all_primes a b =
    if a > b then [] else
      let rest = all_primes (a + 1) b in
      if is_prime a then a :: rest else rest;;
val is_prime : int -> bool = <fun>
val all_primes : int -> int -> int list = <fun>

哥德巴赫猜想

中级

哥德巴赫猜想说,每个大于 2 的正偶数都是两个素数的和。例如:28 = 5 + 23。它是数论中最著名的结论之一,尚未在一般情况下得到证明。它已被数值验证到非常大的数字。编写一个函数来查找加起来等于给定偶数的两个素数。

# goldbach 28;;
- : int * int = (5, 23)
# (* [is_prime] is defined in the previous solution *)
  let goldbach n =
    let rec aux d =
      if is_prime d && is_prime (n - d) then (d, n - d)
      else aux (d + 1)
    in
      aux 2;;
val goldbach : int -> int * int = <fun>

哥德巴赫分解列表

中级

给定一个由下限和上限表示的整数范围,打印所有偶数及其哥德巴赫分解的列表。

在大多数情况下,如果一个偶数写成两个素数的和,则其中一个素数非常小。很少情况下,两个素数都大于例如 50。尝试找出在范围 2..3000 内有多少个这样的情况。

# goldbach_list 9 20;;
- : (int * (int * int)) list =
[(10, (3, 7)); (12, (5, 7)); (14, (3, 11)); (16, (3, 13)); (18, (5, 13));
 (20, (3, 17))]
# (* [goldbach] is defined in the previous question. *)
  let rec goldbach_list a b =
    if a > b then [] else
      if a mod 2 = 1 then goldbach_list (a + 1) b
      else (a, goldbach a) :: goldbach_list (a + 2) b

  let goldbach_limit a b lim =
    List.filter (fun (_, (a, b)) -> a > lim && b > lim) (goldbach_list a b);;
val goldbach_list : int -> int -> (int * (int * int)) list = <fun>
val goldbach_limit : int -> int -> int -> (int * (int * int)) list = <fun>

逻辑表达式的真值表(2 个变量)

中级

让我们定义一个包含变量的布尔表达式的“小型语言”

# type bool_expr =
  | Var of string
  | Not of bool_expr
  | And of bool_expr * bool_expr
  | Or of bool_expr * bool_expr;;
type bool_expr =
    Var of string
  | Not of bool_expr
  | And of bool_expr * bool_expr
  | Or of bool_expr * bool_expr

具有两个变量的逻辑表达式可以用前缀表示法写成。例如,(a ∨ b) ∧ (a ∧ b) 写成

# And (Or (Var "a", Var "b"), And (Var "a", Var "b"));;
- : bool_expr = And (Or (Var "a", Var "b"), And (Var "a", Var "b"))

定义一个函数 table2,它返回给定逻辑表达式(以参数指定)的真值表。返回值必须是一个包含 (value_of_a, value_of_b, value_of_expr) 的三元组列表。

# table2 "a" "b" (And (Var "a", Or (Var "a", Var "b")));;
- : (bool * bool * bool) list =
[(true, true, true); (true, false, true); (false, true, false);
 (false, false, false)]
# let rec eval2 a val_a b val_b = function
    | Var x -> if x = a then val_a
               else if x = b then val_b
               else failwith "The expression contains an invalid variable"
    | Not e -> not (eval2 a val_a b val_b e)
    | And(e1, e2) -> eval2 a val_a b val_b e1 && eval2 a val_a b val_b e2
    | Or(e1, e2) -> eval2 a val_a b val_b e1 || eval2 a val_a b val_b e2
  let table2 a b expr =
    [(true,  true,  eval2 a true  b true  expr);
     (true,  false, eval2 a true  b false expr);
     (false, true,  eval2 a false b true  expr);
     (false, false, eval2 a false b false expr)];;
val eval2 : string -> bool -> string -> bool -> bool_expr -> bool = <fun>
val table2 : string -> string -> bool_expr -> (bool * bool * bool) list =
  <fun>

逻辑表达式的真值表

中级

将前面的问题推广,使逻辑表达式可以包含任意数量的逻辑变量。以一种方式定义 table,使得 table variables expr 返回表达式 expr 的真值表,该表达式包含在 variables 中枚举的逻辑变量。

# table ["a"; "b"] (And (Var "a", Or (Var "a", Var "b")));;
- : ((string * bool) list * bool) list =
[([("a", true); ("b", true)], true); ([("a", true); ("b", false)], true);
 ([("a", false); ("b", true)], false); ([("a", false); ("b", false)], false)]
# (* [val_vars] is an associative list containing the truth value of
     each variable.  For efficiency, a Map or a Hashtlb should be
     preferred. *)

  let rec eval val_vars = function
    | Var x -> List.assoc x val_vars
    | Not e -> not (eval val_vars e)
    | And(e1, e2) -> eval val_vars e1 && eval val_vars e2
    | Or(e1, e2) -> eval val_vars e1 || eval val_vars e2

  (* Again, this is an easy and short implementation rather than an
     efficient one. *)
  let rec table_make val_vars vars expr =
    match vars with
    | [] -> [(List.rev val_vars, eval val_vars expr)]
    | v :: tl ->
         table_make ((v, true) :: val_vars) tl expr
       @ table_make ((v, false) :: val_vars) tl expr

  let table vars expr = table_make [] vars expr;;
val eval : (string * bool) list -> bool_expr -> bool = <fun>
val table_make :
  (string * bool) list ->
  string list -> bool_expr -> ((string * bool) list * bool) list = <fun>
val table : string list -> bool_expr -> ((string * bool) list * bool) list =
  <fun>

格雷码

中级

n 位格雷码是根据特定规则构造的一系列 n 位字符串。例如,

n = 1: C(1) = ['0', '1'].
n = 2: C(2) = ['00', '01', '11', '10'].
n = 3: C(3) = ['000', '001', '011', '010', '110', '111', '101', '100'].

找出构造规则,并编写一个具有以下规范的函数:gray n 返回 n 位格雷码。

# gray 1;;
- : string list = ["0"; "1"]
# gray 2;;
- : string list = ["00"; "01"; "11"; "10"]
# gray 3;;
- : string list = ["000"; "001"; "011"; "010"; "110"; "111"; "101"; "100"]
# let gray n =
    let rec gray_next_level k l =
      if k < n then
        (* This is the core part of the Gray code construction.
         * first_half is reversed and has a "0" attached to every element.
         * Second part is reversed (it must be reversed for correct gray code).
         * Every element has "1" attached to the front.*)
        let (first_half,second_half) =
          List.fold_left (fun (acc1,acc2) x ->
              (("0" ^ x) :: acc1, ("1" ^ x) :: acc2)) ([], []) l
        in
        (* List.rev_append turns first_half around and attaches it to second_half.
         * The result is the modified first_half in correct order attached to
         * the second_half modified in reversed order.*)
        gray_next_level (k + 1) (List.rev_append first_half second_half)
      else l
    in
      gray_next_level 1 ["0"; "1"];;
val gray : int -> string list = <fun>

霍夫曼编码

高级

首先,查阅一本关于离散数学或算法的优秀书籍,以获取对霍夫曼代码的详细描述(你可以从 维基百科页面 开始)!

我们考虑一组带有其频率的符号。例如,如果字母表是 "a",..., "f"(表示为位置 0,...5)并且各自的频率是 45、13、12、16、9、5

# let fs = [("a", 45); ("b", 13); ("c", 12); ("d", 16);
          ("e", 9); ("f", 5)];;
val fs : (string * int) list =
  [("a", 45); ("b", 13); ("c", 12); ("d", 16); ("e", 9); ("f", 5)]

我们的目标是为所有符号 s 构造霍夫曼代码 c 词。在我们的示例中,结果可能是 hs = [("a", "0"); ("b", "101"); ("c", "100"); ("d", "111"); ("e", "1101"); ("f", "1100")](或 hs = [("a", "1");...])。该任务将由定义如下函数 huffman 来执行:huffman(fs) 返回频率表 fs 的霍夫曼代码表。

# huffman fs;;
- : (string * string) list =
[("a", "0"); ("c", "100"); ("b", "101"); ("f", "1100"); ("e", "1101");
 ("d", "111")]
# (* Simple priority queue where the priorities are integers 0..100.
     The node with the lowest probability comes first. *)
  module Pq = struct
    type 'a t = {data: 'a list array; mutable first: int}
    let make() = {data = Array.make 101 []; first = 101}
        let add q p x =
      q.data.(p) <- x :: q.data.(p);  q.first <- min p q.first
          let get_min q =
      if q.first = 101 then None else
        match q.data.(q.first) with
        | [] -> assert false
        | x :: tl ->
           let p = q.first in
           q.data.(q.first) <- tl;
           while q.first < 101 && q.data.(q.first) = [] do
             q.first <- q.first + 1
           done;
           Some(p, x)
  end
    type tree =
    | Leaf of string
    | Node of tree * tree
      let rec huffman_tree q =
    match Pq.get_min q, Pq.get_min q with
    | Some(p1, t1), Some(p2, t2) -> Pq.add q (p1 + p2) (Node(t1, t2));
                                    huffman_tree q
    | Some(_, t), None | None, Some(_, t) -> t
    | None, None -> assert false
      (* Build the prefix-free binary code from the tree *)
  let rec prefixes_of_tree prefix = function
    | Leaf s -> [(s, prefix)]
    | Node(t0, t1) ->  prefixes_of_tree (prefix ^ "0") t0
                     @ prefixes_of_tree (prefix ^ "1") t1
                       let huffman fs =
    if List.fold_left (fun s (_, p) -> s + p) 0 fs <> 100 then
      failwith "huffman: sum of weights must be 100";
    let q = Pq.make () in
    List.iter (fun (s, f) -> Pq.add q f (Leaf s)) fs;
    prefixes_of_tree "" (huffman_tree q);;
module Pq :
  sig
    type 'a t = { data : 'a list array; mutable first : int; }
    val make : unit -> 'a t
    val add : 'a t -> int -> 'a -> unit
    val get_min : 'a t -> (int * 'a) option
  end
type tree = Leaf of string | Node of tree * tree
val huffman_tree : tree Pq.t -> tree = <fun>
val prefixes_of_tree : string -> tree -> (string * string) list = <fun>
val huffman : (string * int) list -> (string * string) list = <fun>

构造完全平衡的二叉树

中级

Binary Tree

二叉树要么为空,要么由一个根元素和两个后继组成,它们本身也是二叉树。

在 OCaml 中,可以定义一个新的类型 binary_tree,它在每个节点处携带类型 'a 的任意值(因此是多态的)。

# type 'a binary_tree =
  | Empty
  | Node of 'a * 'a binary_tree * 'a binary_tree;;
type 'a binary_tree = Empty | Node of 'a * 'a binary_tree * 'a binary_tree

携带 char 数据的树的示例是

# let example_tree =
  Node ('a', Node ('b', Node ('d', Empty, Empty), Node ('e', Empty, Empty)),
       Node ('c', Empty, Node ('f', Node ('g', Empty, Empty), Empty)));;
val example_tree : char binary_tree =
  Node ('a', Node ('b', Node ('d', Empty, Empty), Node ('e', Empty, Empty)),
   Node ('c', Empty, Node ('f', Node ('g', Empty, Empty), Empty)))

在 OCaml 中,严格的类型约束保证,如果你得到一个 binary_tree 类型的的值,那么它一定是用两个构造函数 EmptyNode 创建的。

在完全平衡的二叉树中,对于每个节点都满足以下属性:其左子树中的节点数量与其右子树中的节点数量几乎相等,这意味着它们之间的差值不超过 1。

编写一个函数cbal_tree,用于为给定数量的节点构建完全平衡的二叉树。该函数应通过回溯生成所有解决方案。将字母'x'作为信息放入树的所有节点中。

# cbal_tree 4;;
- : char binary_tree/2 list =
[Node ('x', Node ('x', Empty, Empty),
  Node ('x', Node ('x', Empty, Empty), Empty));
 Node ('x', Node ('x', Empty, Empty),
  Node ('x', Empty, Node ('x', Empty, Empty)));
 Node ('x', Node ('x', Node ('x', Empty, Empty), Empty),
  Node ('x', Empty, Empty));
 Node ('x', Node ('x', Empty, Node ('x', Empty, Empty)),
  Node ('x', Empty, Empty))]
# (* Build all trees with given [left] and [right] subtrees. *)
  let add_trees_with left right all =
    let add_right_tree all l =
      List.fold_left (fun a r -> Node ('x', l, r) :: a) all right in
    List.fold_left add_right_tree all left

  let rec cbal_tree n =
    if n = 0 then [Empty]
    else if n mod 2 = 1 then
      let t = cbal_tree (n / 2) in
      add_trees_with t t []
    else (* n even: n-1 nodes for the left & right subtrees altogether. *)
      let t1 = cbal_tree (n / 2 - 1) in
      let t2 = cbal_tree (n / 2) in
      add_trees_with t1 t2 (add_trees_with t2 t1 []);;
val add_trees_with :
  char binary_tree list ->
  char binary_tree list -> char binary_tree list -> char binary_tree list =
  <fun>
val cbal_tree : int -> char binary_tree list = <fun>

对称二叉树

中级

如果可以从根节点画一条垂直线,然后右子树是左子树的镜像,我们就称一个二叉树是对称的。编写一个函数is_symmetric来检查给定的二叉树是否是对称的。

提示:首先编写一个函数is_mirror来检查一棵树是否与另一棵树镜像。我们只关心结构,而不关心节点的内容。

# let rec is_mirror t1 t2 =
    match t1, t2 with
    | Empty, Empty -> true
    | Node(_, l1, r1), Node(_, l2, r2) ->
       is_mirror l1 r2 && is_mirror r1 l2
    | _ -> false

  let is_symmetric = function
    | Empty -> true
    | Node(_, l, r) -> is_mirror l r;;
val is_mirror : 'a binary_tree -> 'b binary_tree -> bool = <fun>
val is_symmetric : 'a binary_tree -> bool = <fun>

二叉搜索树(字典)

中级

从整数列表中构建一个二叉搜索树

# construct [3; 2; 5; 7; 1];;
- : int binary_tree =
Node (3, Node (2, Node (1, Empty, Empty), Empty),
 Node (5, Empty, Node (7, Empty, Empty)))

然后使用此函数测试先前问题的解决方案。

# is_symmetric (construct [5; 3; 18; 1; 4; 12; 21]);;
- : bool = true
# not (is_symmetric (construct [3; 2; 5; 7; 4]));;
- : bool = true
# let rec insert tree x = match tree with
    | Empty -> Node (x, Empty, Empty)
    | Node (y, l, r) ->
       if x = y then tree
       else if x < y then Node (y, insert l x, r)
       else Node (y, l, insert r x)
  let construct l = List.fold_left insert Empty l;;
val insert : 'a binary_tree -> 'a -> 'a binary_tree = <fun>
val construct : 'a list -> 'a binary_tree = <fun>

生成-测试范式

中级

应用生成-测试范式来构建具有给定节点数量的所有对称、完全平衡的二叉树。

# sym_cbal_trees 5;;
- : char binary_tree list =
[Node ('x', Node ('x', Node ('x', Empty, Empty), Empty),
  Node ('x', Empty, Node ('x', Empty, Empty)));
 Node ('x', Node ('x', Empty, Node ('x', Empty, Empty)),
  Node ('x', Node ('x', Empty, Empty), Empty))]

有多少棵这样的树有 57 个节点?调查对于给定数量的节点有多少个解决方案?如果数量是偶数怎么办?编写一个合适的函数。

# List.length (sym_cbal_trees 57);;
- : int = 256
# let sym_cbal_trees n =
    List.filter is_symmetric (cbal_tree n);;
val sym_cbal_trees : int -> char binary_tree list = <fun>

构建高度平衡的二叉树

中级

在高度平衡的二叉树中,以下属性对每个节点都成立:其左子树的高度与其右子树的高度几乎相等,这意味着它们之间的差值不超过 1。

编写一个函数hbal_tree来为给定高度构建高度平衡的二叉树。该函数应通过回溯生成所有解决方案。将字母'x'作为信息放入树的所有节点中。

# let t = hbal_tree 3;;
val t : char binary_tree list =
  [Node ('x', Node ('x', Empty, Node ('x', Empty, Empty)),
    Node ('x', Empty, Node ('x', Empty, Empty)));
   Node ('x', Node ('x', Empty, Node ('x', Empty, Empty)),
    Node ('x', Node ('x', Empty, Empty), Empty));
   Node ('x', Node ('x', Empty, Node ('x', Empty, Empty)),
    Node ('x', Node ('x', Empty, Empty), Node ('x', Empty, Empty)));
   Node ('x', Node ('x', Node ('x', Empty, Empty), Empty),
    Node ('x', Empty, Node ('x', Empty, Empty)));
   Node ('x', Node ('x', Node ('x', Empty, Empty), Empty),
    Node ('x', Node ('x', Empty, Empty), Empty));
   Node ('x', Node ('x', Node ('x', Empty, Empty), Empty),
    Node ('x', Node ('x', Empty, Empty), Node ('x', Empty, Empty)));
   Node ('x', Node ('x', Node ('x', Empty, Empty), Node ('x', Empty, Empty)),
    Node ('x', Empty, Node ('x', Empty, Empty)));
   Node ('x', Node ('x', Node ('x', Empty, Empty), Node ('x', Empty, Empty)),
    Node ('x', Node ('x', Empty, Empty), Empty));
   Node ('x', Node ('x', Node ('x', Empty, Empty), Node ('x', Empty, Empty)),
    Node ('x', Node ('x', Empty, Empty), Node ('x', Empty, Empty)));
   Node ('x', Node ('x', Empty, Node ('x', Empty, Empty)),
    Node ('x', Empty, Empty));
   Node ('x', Node ('x', Node ('x', Empty, Empty), Empty),
    Node ('x', Empty, Empty));
   Node ('x', Node ('x', Node ('x', Empty, Empty), Node ('x', Empty, Empty)),
    Node ('x', Empty, Empty));
   Node ('x', Node ('x', Empty, Empty),
    Node ('x', Empty, Node ('x', Empty, Empty)));
   Node ('x', Node ('x', Empty, Empty),
    Node ('x', Node ('x', Empty, Empty), Empty));
   Node ('x', Node ('x', Empty, Empty),
    Node ('x', Node ('x', Empty, Empty), Node ('x', Empty, Empty)))]

函数add_trees_with构建完全平衡的二叉树的解决方案中定义。

# let rec hbal_tree n =
    if n = 0 then [Empty]
    else if n = 1 then [Node ('x', Empty, Empty)]
    else
    (* [add_trees_with left right trees] is defined in a question above. *)
      let t1 = hbal_tree (n - 1)
      and t2 = hbal_tree (n - 2) in
      add_trees_with t1 t1 (add_trees_with t1 t2 (add_trees_with t2 t1 []));;
val hbal_tree : int -> char binary_tree list = <fun>

构建具有给定节点数量的高度平衡的二叉树

中级

考虑高度为h的高度平衡二叉树。它可以包含的最大节点数是多少?显然,max_nodes = 2h - 1。

# let max_nodes h = 1 lsl h - 1;;
val max_nodes : int -> int = <fun>

最小节点

但是,最小节点数min_nodes是多少?这个问题更难。尝试找到一个递归语句并将其转换为一个函数min_nodes,定义如下:min_nodes h返回高度为h的高度平衡二叉树中的最小节点数。

最小高度

另一方面,我们可能会问:具有 N 个节点的高度平衡二叉树的最小(分别最大)高度 H 可以是多少?min_height(分别为max_height n)返回具有n个节点的高度平衡二叉树的最小(分别为最大)高度。

构建树

现在,我们可以解决主要问题:构建具有给定节点数量的所有高度平衡二叉树。hbal_tree_nodes n返回具有n个节点的所有高度平衡二叉树的列表。

找出n = 15时存在多少个高度平衡的树。

# List.length (hbal_tree_nodes 15);;
- : int = 1553

最小节点

以下解决方案直接来自翻译问题。

# let rec min_nodes h =
    if h <= 0 then 0 
    else if h = 1 then 1
    else min_nodes (h - 1) + min_nodes (h - 2) + 1;;
val min_nodes : int -> int = <fun>

然而,它不是最有效的。应该使用最后两个值作为状态以避免双重递归。

# let rec min_nodes_loop m0 m1 h =
    if h <= 1 then m1
    else min_nodes_loop m1 (m1 + m0 + 1) (h - 1)
    let min_nodes h =
    if h <= 0 then 0 else min_nodes_loop 0 1 h;;
val min_nodes_loop : int -> int -> int -> int = <fun>
val min_nodes : int -> int = <fun>

不难证明min_nodes h = Fh+2‌ - 1,其中(Fn)是斐波那契数列

最小高度

反转公式 max_nodes = 2h - 1,可以直接找到 Hₘᵢₙ(n) = ⌈log₂(n+1)⌉,它很容易实现

# let min_height n = int_of_float (ceil (log (float(n + 1)) /. log 2.));;
val min_height : int -> int = <fun>

让我们证明 Hₘᵢₙ 公式的有效性。首先,如果 h = min_height n,则存在一个高度为 h 且具有 n 个节点的高度平衡树。因此 2ʰ - 1 = max_nodes h ≥ n,即 h ≥ log₂(n+1)。为了确定 Hₘᵢₙ(n) 的相等性,必须证明对于任何 n,都存在一个高度为 Hₘᵢₙ(n) 的高度平衡树。这是由于关系 Hₘᵢₙ(n) = 1 + Hₘᵢₙ(n/2),其中 n/2 是整数除法。对于 n 为奇数,这很容易证明——所以可以构建一个具有顶点和两个子树的树,这两个子树具有 n/2 个高度为 Hₘᵢₙ(n) - 1 的节点。对于 n 为偶数,如果首先注意到在这种情况下,⌈log₂(n+2)⌉ = ⌈log₂(n+1)⌉,则可以使用相同的证明——使用 log₂(n+1) ≤ h ∈ ℕ ⇔ 2ʰ ≥ n + 1 以及 2ʰ 对于此为偶数的事实。这允许有一个具有 n/2 个节点的子树。对于另一个具有 n/2-1 个节点的子树,必须确定 Hₘᵢₙ(n/2-1) ≥ Hₘᵢₙ(n) - 2,这很简单,因为如果 h = Hₘᵢₙ(n/2-1),则 h+2 ≥ log₂(2n) ≥ log₂(n+1)。

然而,上面的函数并不是最好的。实际上,并非每个 64 位整数都可以精确地表示为浮点数。这里有一个只使用整数运算的函数

# let rec ceil_log2_loop log plus1 n =
    if n = 1 then if plus1 then log + 1 else log
    else ceil_log2_loop (log + 1) (plus1 || n land 1 <> 0) (n / 2)
    let ceil_log2 n = ceil_log2_loop 0 false n;;
val ceil_log2_loop : int -> bool -> int -> int = <fun>
val ceil_log2 : int -> int = <fun>

然而,该算法仍然不是最快的。例如,参见黑客乐园,第 5-3 节(以及 11-4 节)。

遵循与上面相同的思路,如果 h = max_height n,则可以很容易地推导出 min_nodes h ≤ n < min_nodes(h+1)。这产生了以下代码

# let rec max_height_search h n =
    if min_nodes h <= n then max_height_search (h + 1) n else h - 1
  let max_height n = max_height_search 0 n;;
val max_height_search : int -> int -> int = <fun>
val max_height : int -> int = <fun>

当然,由于min_nodes是递归计算的,因此无需重新计算所有内容即可从min_nodes hmin_nodes(h+1)

# let rec max_height_search h m_h m_h1 n =
    if m_h <= n then max_height_search (h + 1) m_h1 (m_h1 + m_h + 1) n else h - 1
    let max_height n = max_height_search 0 0 1 n;;
val max_height_search : int -> int -> int -> int -> int = <fun>
val max_height : int -> int = <fun>

构建树

首先,我们定义一些便利函数fold_range,它在范围n0...n1上折叠函数f,即它计算f (... f (f (f init n0) (n0+1)) (n0+2) ...) n1。可以将其视为执行赋值init ← f init n,其中n = n0,..., n1,除了代码中没有可变变量。

# let rec fold_range ~f ~init n0 n1 =
    if n0 > n1 then init else fold_range ~f ~init:(f init n0) (n0 + 1) n1;;
val fold_range : f:('a -> int -> 'a) -> init:'a -> int -> int -> 'a = <fun>

在构建树时,存在明显的对称性:如果交换平衡树的左子树和右子树,我们仍然有一个平衡树。以下函数返回trees中的所有树及其排列。

# let rec add_swap_left_right trees =
    List.fold_left (fun a n -> match n with
                               | Node (v, t1, t2) -> Node (v, t2, t1) :: a
                               | Empty -> a) trees trees;;
val add_swap_left_right : 'a binary_tree list -> 'a binary_tree list = <fun>

最后,我们使用先验计算的边界递归地生成所有树。它可以进一步优化,但我们的目标是直接表达这个想法。

# let rec hbal_tree_nodes_height h n =
    assert(min_nodes h <= n && n <= max_nodes h);
    if h = 0 then [Empty]
    else
      let acc = add_hbal_tree_node [] (h - 1) (h - 2) n in
      let acc = add_swap_left_right acc in
      add_hbal_tree_node acc (h - 1) (h - 1) n
  and add_hbal_tree_node l h1 h2 n =
    let min_n1 = max (min_nodes h1) (n - 1 - max_nodes h2) in
    let max_n1 = min (max_nodes h1) (n - 1 - min_nodes h2) in
    fold_range min_n1 max_n1 ~init:l ~f:(fun l n1 ->
        let t1 = hbal_tree_nodes_height h1 n1 in
        let t2 = hbal_tree_nodes_height h2 (n - 1 - n1) in
        List.fold_left (fun l t1 ->
            List.fold_left (fun l t2 -> Node ('x', t1, t2) :: l) l t2) l t1
      )
      let hbal_tree_nodes n =
    fold_range (min_height n) (max_height n) ~init:[] ~f:(fun l h ->
        List.rev_append (hbal_tree_nodes_height h n) l);;
val hbal_tree_nodes_height : int -> int -> char binary_tree list = <fun>
val add_hbal_tree_node :
  char binary_tree list -> int -> int -> int -> char binary_tree list = <fun>
val hbal_tree_nodes : int -> char binary_tree list = <fun>

统计二叉树的叶子数

初级

叶子是没有任何后继节点的节点。编写一个函数count_leaves来统计它们。

# count_leaves Empty;;
- : int = 0
# let rec count_leaves = function
    | Empty -> 0
    | Node (_, Empty, Empty) -> 1
    | Node (_, l, r) -> count_leaves l + count_leaves r;;
val count_leaves : 'a binary_tree -> int = <fun>

将二叉树的叶子收集到列表中

初级

叶子是没有任何后继节点的节点。编写一个函数leaves来将它们收集到列表中。

# leaves Empty;;
- : 'a list = []
# (* Having an accumulator acc prevents using inefficient List.append.
   * Every Leaf will be pushed directly into accumulator.
   * Not tail-recursive, but that is no problem since we have a binary tree and
   * and stack depth is logarithmic. *)
  let leaves t = 
    let rec leaves_aux t acc = match t with
      | Empty -> acc
      | Node (x, Empty, Empty) -> x :: acc
      | Node (x, l, r) -> leaves_aux l (leaves_aux r acc)
    in
    leaves_aux t [];;
val leaves : 'a binary_tree -> 'a list = <fun>

将二叉树的内部节点收集到列表中

初级

二叉树的内部节点有一个或两个非空后继节点。编写一个函数internals来将它们收集到列表中。

# internals (Node ('a', Empty, Empty));;
- : char list = []
# (* Having an accumulator acc prevents using inefficient List.append.
   * Every internal node will be pushed directly into accumulator.
   * Not tail-recursive, but that is no problem since we have a binary tree and
   * and stack depth is logarithmic. *)
  let internals t = 
    let rec internals_aux t acc = match t with
      | Empty -> acc
      | Node (x, Empty, Empty) -> acc
      | Node (x, l, r) -> internals_aux l (x :: internals_aux r acc)
    in
    internals_aux t [];;
val internals : 'a binary_tree -> 'a list = <fun>

将给定级别的节点收集到列表中

初级

如果从根节点到节点的路径长度为 N-1,则二叉树的节点位于 N 级。根节点位于 1 级。编写一个函数at_level t l来将树t中位于l级的所有节点收集到列表中。

# let example_tree =
  Node ('a', Node ('b', Node ('d', Empty, Empty), Node ('e', Empty, Empty)),
       Node ('c', Empty, Node ('f', Node ('g', Empty, Empty), Empty)));;
val example_tree : char binary_tree =
  Node ('a', Node ('b', Node ('d', Empty, Empty), Node ('e', Empty, Empty)),
   Node ('c', Empty, Node ('f', Node ('g', Empty, Empty), Empty)))
# at_level example_tree 2;;
- : char list = ['b'; 'c']

使用at_level很容易构建一个函数levelorder,它创建节点的层序序列。但是,还有更有效的方法来做到这一点。

# (* Having an accumulator acc prevents using inefficient List.append.
   * Every node at level N will be pushed directly into accumulator.
   * Not tail-recursive, but that is no problem since we have a binary tree and
   * and stack depth is logarithmic. *)
  let at_level t level =
    let rec at_level_aux t acc counter = match t with
      | Empty -> acc
      | Node (x, l, r) ->
        if counter=level then
          x :: acc
        else
          at_level_aux l (at_level_aux r acc (counter + 1)) (counter + 1)
    in
      at_level_aux t [] 1;;
val at_level : 'a binary_tree -> int -> 'a list = <fun>

构建一棵完全二叉树

中级

高度为 H 的完全二叉树定义如下:级别 1、2、3、...、H-1 包含最大数量的节点(即 2i-1 在 i 级,注意我们从根节点的 1 开始计数级别)。在级别 H 中,它可能包含少于最大可能数量的节点,所有节点都“左对齐”。这意味着在层序树遍历中,所有内部节点都先出现,叶子节点都后出现,空后继节点(实际上不是节点的 nil!)最后出现。

特别是,完全二叉树用作堆的数据结构(或寻址方案)。

我们可以通过按层序枚举节点(从根节点开始编号为 1)来为完全二叉树中的每个节点分配一个地址号。在这样做时,我们意识到对于每个地址为 A 的节点 X,以下属性成立:X 的左和右后继节点的地址分别为 2*A 和 2*A+1,前提是后继节点存在。这一事实可以用于优雅地构建完全二叉树结构。编写一个函数is_complete_binary_tree,具有以下规范:is_complete_binary_tree n t 当且仅当t是具有n个节点的完全二叉树时返回true

# complete_binary_tree [1; 2; 3; 4; 5; 6];;
- : int binary_tree =
Node (1, Node (2, Node (4, Empty, Empty), Node (5, Empty, Empty)),
 Node (3, Node (6, Empty, Empty), Empty))
# let rec split_n lst acc n = match (n, lst) with
    | (0, _) -> (List.rev acc, lst)
    | (_, []) -> (List.rev acc, [])
    | (_, h :: t) -> split_n t (h :: acc) (n-1)

  let rec myflatten p c = 
    match (p, c) with
    | (p, []) -> List.map (fun x -> Node (x, Empty, Empty)) p
    | (x :: t, [y]) -> Node (x, y, Empty) :: myflatten t []
    | (ph :: pt, x :: y :: t) -> (Node (ph, x, y)) :: myflatten pt t
    | _ -> invalid_arg "myflatten"

  let complete_binary_tree = function
    | [] -> Empty
    | lst ->
       let rec aux l = function
         | [] -> []
         | lst -> let p, c = split_n lst [] (1 lsl l) in
                  myflatten p (aux (l + 1) c)
       in
         List.hd (aux 0 lst);;
val split_n : 'a list -> 'a list -> int -> 'a list * 'a list = <fun>
val myflatten : 'a list -> 'a binary_tree list -> 'a binary_tree list = <fun>
val complete_binary_tree : 'a list -> 'a binary_tree = <fun>

布局一棵二叉树(1)

中级

作为绘制树的准备工作,需要一个布局算法来确定每个节点在矩形网格中的位置。可以设想几种布局方法,其中一种方法如插图所示。

Binary Tree Grid

在此布局策略中,节点 v 的位置由以下两个规则获得

  • x(v) 等于节点 v 在中序序列中的位置;
  • y(v) 等于节点v在树中的深度。

为了存储节点的位置,我们将用位置(x,y)丰富每个节点的值。

上面图片中的树是

# let example_layout_tree =
  let leaf x = Node (x, Empty, Empty) in
  Node ('n', Node ('k', Node ('c', leaf 'a',
                           Node ('h', Node ('g', leaf 'e', Empty), Empty)),
                 leaf 'm'),
       Node ('u', Node ('p', Empty, Node ('s', leaf 'q', Empty)), Empty));;
val example_layout_tree : char binary_tree =
  Node ('n',
   Node ('k',
    Node ('c', Node ('a', Empty, Empty),
     Node ('h', Node ('g', Node ('e', Empty, Empty), Empty), Empty)),
    Node ('m', Empty, Empty)),
   Node ('u', Node ('p', Empty, Node ('s', Node ('q', Empty, Empty), Empty)),
    Empty))
# layout_binary_tree_1 example_layout_tree;;
- : (char * int * int) binary_tree =
Node (('n', 8, 1),
 Node (('k', 6, 2),
  Node (('c', 2, 3), Node (('a', 1, 4), Empty, Empty),
   Node (('h', 5, 4),
    Node (('g', 4, 5), Node (('e', 3, 6), Empty, Empty), Empty), Empty)),
  Node (('m', 7, 3), Empty, Empty)),
 Node (('u', 12, 2),
  Node (('p', 9, 3), Empty,
   Node (('s', 11, 4), Node (('q', 10, 5), Empty, Empty), Empty)),
  Empty))
# let layout_binary_tree_1 t =
    let rec layout depth x_left = function
      (* This function returns a pair: the laid out tree and the first
       * free x location *)
      | Empty -> (Empty, x_left)
      | Node (v,l,r) ->
         let (l', l_x_max) = layout (depth + 1) x_left l in
         let (r', r_x_max) = layout (depth + 1) (l_x_max + 1) r in
           (Node ((v, l_x_max, depth), l', r'), r_x_max)
    in
      fst (layout 1 1 t);;
val layout_binary_tree_1 : 'a binary_tree -> ('a * int * int) binary_tree =
  <fun>

布局一棵二叉树(2)

中级

Binary Tree Grid

此插图中描述了另一种布局方法。找出规则并编写相应的 OCaml 函数。

提示:在给定级别上,相邻节点之间的水平距离是恒定的。

显示的树是

# let example_layout_tree =
  let leaf x = Node (x, Empty, Empty) in
  Node ('n', Node ('k', Node ('c', leaf 'a',
                           Node ('e', leaf 'd', leaf 'g')),
                 leaf 'm'),
       Node ('u', Node ('p', Empty, leaf 'q'), Empty));;
val example_layout_tree : char binary_tree =
  Node ('n',
   Node ('k',
    Node ('c', Node ('a', Empty, Empty),
     Node ('e', Node ('d', Empty, Empty), Node ('g', Empty, Empty))),
    Node ('m', Empty, Empty)),
   Node ('u', Node ('p', Empty, Node ('q', Empty, Empty)), Empty))
# layout_binary_tree_2 example_layout_tree ;;
- : (char * int * int) binary_tree =
Node (('n', 15, 1),
 Node (('k', 7, 2),
  Node (('c', 3, 3), Node (('a', 1, 4), Empty, Empty),
   Node (('e', 5, 4), Node (('d', 4, 5), Empty, Empty),
    Node (('g', 6, 5), Empty, Empty))),
  Node (('m', 11, 3), Empty, Empty)),
 Node (('u', 23, 2),
  Node (('p', 19, 3), Empty, Node (('q', 21, 4), Empty, Empty)), Empty))
# let layout_binary_tree_2 t =
    let rec height = function
      | Empty -> 0
      | Node (_, l, r) -> 1 + max (height l) (height r) in
    let tree_height = height t in
    let rec find_missing_left depth = function
      | Empty -> tree_height - depth
      | Node (_, l, _) -> find_missing_left (depth + 1) l in
    let translate_dst = 1 lsl (find_missing_left 0 t) - 1 in
                        (* remember than 1 lsl a = 2ᵃ *)
    let rec layout depth x_root = function
      | Empty -> Empty
      | Node (x, l, r) ->
         let spacing = 1 lsl (tree_height - depth - 1) in
         let l' = layout (depth + 1) (x_root - spacing) l
         and r' = layout (depth + 1) (x_root + spacing) r in
           Node((x, x_root, depth), l',r') in
    layout 1 ((1 lsl (tree_height - 1)) - translate_dst) t;;
val layout_binary_tree_2 : 'a binary_tree -> ('a * int * int) binary_tree =
  <fun>

布局一棵二叉树(3)

高级

Binary Tree Grid

上面的插图显示了另一种布局策略。该方法产生了非常紧凑的布局,同时在每个节点上保持一定程度的对称性。找出规则并编写相应的谓词。

提示:考虑节点与其后继节点之间的水平距离。可以将两个子树紧密地打包在一起以构建组合的二叉树?这是一个难题。不要过早放弃!

# let example_layout_tree =
  let leaf x = Node (x, Empty, Empty) in
  Node ('n', Node ('k', Node ('c', leaf 'a',
                           Node ('h', Node ('g', leaf 'e', Empty), Empty)),
                 leaf 'm'),
       Node ('u', Node ('p', Empty, Node ('s', leaf 'q', Empty)), Empty));;
val example_layout_tree : char binary_tree =
  Node ('n',
   Node ('k',
    Node ('c', Node ('a', Empty, Empty),
     Node ('h', Node ('g', Node ('e', Empty, Empty), Empty), Empty)),
    Node ('m', Empty, Empty)),
   Node ('u', Node ('p', Empty, Node ('s', Node ('q', Empty, Empty), Empty)),
    Empty))
# layout_binary_tree_3 example_layout_tree ;;
- : (char * int * int) binary_tree =
Node (('n', 5, 1),
 Node (('k', 3, 2),
  Node (('c', 2, 3), Node (('a', 1, 4), Empty, Empty),
   Node (('h', 3, 4),
    Node (('g', 2, 5), Node (('e', 1, 6), Empty, Empty), Empty), Empty)),
  Node (('m', 4, 3), Empty, Empty)),
 Node (('u', 7, 2),
  Node (('p', 6, 3), Empty,
   Node (('s', 7, 4), Node (('q', 6, 5), Empty, Empty), Empty)),
  Empty))

您最喜欢哪种布局?

为了紧密地打包树,布局函数将除了树的布局之外还返回树的左右轮廓,即相对于树的根节点位置的偏移列表。

# let layout_binary_tree_3 =
    let rec translate_x d = function
      | Empty -> Empty
      | Node ((v, x, y), l, r) ->
         Node ((v, x + d, y), translate_x d l, translate_x d r) in
    (* Distance between a left subtree given by its right profile [lr]
       and a right subtree given by its left profile [rl]. *)
    let rec dist lr rl = match lr, rl with
      | lrx :: ltl, rlx :: rtl -> max (lrx - rlx) (dist ltl rtl)
      | [], _ | _, [] -> 0 in
    let rec merge_profiles p1 p2 = match p1, p2 with
      | x1 :: tl1, _ :: tl2 -> x1 :: merge_profiles tl1 tl2
      | [], _ -> p2
      | _, [] -> p1 in
    let rec layout depth = function
      | Empty -> ([], Empty, [])
      | Node (v, l, r) ->
         let (ll, l', lr) = layout (depth + 1) l in
         let (rl, r', rr) = layout (depth + 1) r in
         let d = 1 + dist lr rl / 2 in
         let ll = List.map (fun x -> x - d) ll
         and lr = List.map (fun x -> x - d) lr
         and rl = List.map ((+) d) rl
         and rr = List.map ((+) d) rr in
         (0 :: merge_profiles ll rl,
          Node((v, 0, depth), translate_x (-d) l', translate_x d r'),
          0 :: merge_profiles rr lr) in
    fun t -> let (l, t', _) = layout 1 t in
             let x_min = List.fold_left min 0 l in
             translate_x (1 - x_min) t';;
val layout_binary_tree_3 : 'a binary_tree -> ('a * int * int) binary_tree =
  <fun>

二叉树的字符串表示

中级

Binary Tree

有人将二叉树表示为以下类型的字符串(见示例):"a(b(d,e),c(,f(g,)))"

  • 编写一个 OCaml 函数string_of_tree,如果树以通常的方式给出(作为EmptyNode(x,l,r) 项),则该函数将生成此字符串表示。然后编写一个函数tree_of_string,它执行此反向操作;即,给定字符串表示,以通常形式构建树。最后,将这两个谓词组合到一个单独的函数tree_string中,该函数可以双向使用。
  • 使用差分列表和一个单独的谓词tree_dlist编写相同的谓词tree_string,该谓词在树和差分列表之间双向执行转换。

为了简单起见,假设节点中的信息是一个字母,字符串中没有空格。

# let example_layout_tree =
  let leaf x = Node (x, Empty, Empty) in
    (Node ('a', Node ('b', leaf 'd', leaf 'e'),
     Node ('c', Empty, Node ('f', leaf 'g', Empty))));;
val example_layout_tree : char binary_tree =
  Node ('a', Node ('b', Node ('d', Empty, Empty), Node ('e', Empty, Empty)),
   Node ('c', Empty, Node ('f', Node ('g', Empty, Empty), Empty)))

一个简单的解决方案是

# let rec string_of_tree = function
    | Empty -> ""
    | Node(data, l, r) ->
       let data = String.make 1 data in
       match l, r with
       | Empty, Empty -> data
       | _, _ -> data ^ "(" ^ (string_of_tree l)
                 ^ "," ^ (string_of_tree r) ^ ")";;
val string_of_tree : char binary_tree -> string = <fun>

也可以使用缓冲区来分配更少的内存

# let rec buffer_add_tree buf = function
    | Empty -> ()
    | Node (data, l, r) ->
       Buffer.add_char buf data;
       match l, r with
       | Empty, Empty -> ()
       | _, _ -> Buffer.add_char buf '(';
                 buffer_add_tree buf l;
                 Buffer.add_char buf ',';
                 buffer_add_tree buf r;
                 Buffer.add_char buf ')'
                 let string_of_tree t =
    let buf = Buffer.create 128 in
      buffer_add_tree buf t;
      Buffer.contents buf;;
val buffer_add_tree : Buffer.t -> char binary_tree -> unit = <fun>
val string_of_tree : char binary_tree -> string = <fun>

对于反向转换,我们假设字符串格式正确,并且不处理错误报告。

# let tree_of_string =
    let rec make ofs s =
      if ofs >= String.length s || s.[ofs] = ',' || s.[ofs] = ')' then
        (Empty, ofs)
      else
        let v = s.[ofs] in
        if ofs + 1 < String.length s && s.[ofs + 1] = '(' then
          let l, ofs = make (ofs + 2) s in (* skip "v(" *)
          let r, ofs = make (ofs + 1) s in (* skip "," *)
            (Node (v, l, r), ofs + 1) (* skip ")" *)
        else (Node (v, Empty, Empty), ofs + 1)
    in
      fun s -> fst (make 0 s);;
val tree_of_string : string -> char binary_tree = <fun>

二叉树的前序和中序序列

中级

我们考虑二叉树,其节点由单个小写字母标识,如上一个问题的示例所示。

  1. 编写函数 preorderinorder,分别构建给定二叉树的 前序中序 序列。结果应该是原子,例如,上一个问题中示例的前序序列为 'abdecfg'。
  2. 你能将问题部分 1 中的 preorder 反向使用吗?即,给定一个前序序列,构建相应的树?如果不是,请进行必要的调整。
  3. 如果给定了二叉树节点的前序序列和中序序列,那么树将被唯一确定。编写一个函数 pre_in_tree 来完成这项工作。
  4. 使用 差分列表 解决问题 1 到 3。酷!使用函数 timeit(在问题“比较计算欧拉总函数的两种方法。”中定义)来比较这些解决方案。

如果同一个字符出现在多个节点中会发生什么?例如,尝试 pre_in_tree "aba" "baa"

# preorder (Node (1, Node (2, Empty, Empty), Empty));;
- : int list = [1; 2]

我们使用列表来表示结果。请注意,preorderinorder 可以通过避免列表串联来提高效率。

# let rec preorder = function
    | Empty -> []
    | Node (v, l, r) -> v :: (preorder l @ preorder r)
    let rec inorder = function
    | Empty -> []
    | Node (v, l, r) -> inorder l @ (v :: inorder r)
    let rec split_pre_in p i x accp acci = match (p, i) with
    | [], [] -> (List.rev accp, List.rev acci), ([], [])
    | h1 :: t1, h2 :: t2 ->
       if x = h2 then
         (List.tl (List.rev (h1 :: accp)), t1),
         (List.rev (List.tl (h2 :: acci)), t2)
       else
         split_pre_in t1 t2 x (h1 :: accp) (h2 :: acci)
    | _ -> assert false
    let rec pre_in_tree p i = match (p, i) with
    | [], [] -> Empty
    | (h1 :: t1), (h2 :: t2) ->
       let (lp, rp), (li, ri) = split_pre_in p i h1 [] [] in
         Node (h1, pre_in_tree lp li, pre_in_tree rp ri)
    | _ -> invalid_arg "pre_in_tree";;
val preorder : 'a binary_tree -> 'a list = <fun>
val inorder : 'a binary_tree -> 'a list = <fun>
val split_pre_in :
  'a list ->
  'a list ->
  'a -> 'a list -> 'a list -> ('a list * 'a list) * ('a list * 'a list) =
  <fun>
val pre_in_tree : 'a list -> 'a list -> 'a binary_tree = <fun>

使用 差分列表 的解决方案。

  (* solution pending *)

二叉树的点字符串表示

中级

我们再次考虑二叉树,其节点由单个小写字母标识,如问题“二叉树的字符串表示”中的示例所示。这样的树可以通过其节点的前序序列来表示,在其中插入点 (.),以便在树遍历期间遇到空子树 (nil) 时插入点。例如,问题“二叉树的字符串表示”中显示的树表示为 'abd..e..c.fg...'。首先,尝试建立一个语法(BNF 或语法图),然后编写一个函数 tree_dotstring,它可以双向进行转换。使用差分列表。

  (* solution pending *)

从节点字符串构建树

中级

Multiway Tree

多路树由根元素和(可能为空的)一组后继组成,这些后继本身也是多路树。多路树永远不会为空。后继树集有时被称为森林。

为了表示多路树,我们将使用以下类型,它是定义的直接翻译

# type 'a mult_tree = T of 'a * 'a mult_tree list;;
type 'a mult_tree = T of 'a * 'a mult_tree list

因此,对面显示的示例树由以下 OCaml 表达式表示

# T ('a', [T ('f', [T ('g', [])]); T ('c', []); T ('b', [T ('d', []); T ('e', [])])]);;
- : char mult_tree =
T ('a',
 [T ('f', [T ('g', [])]); T ('c', []); T ('b', [T ('d', []); T ('e', [])])])

我们假设多路树的节点包含单个字符。在其节点的深度优先顺序序列中,每当在树遍历期间移动是回溯到前一级时,都会插入一个特殊字符 ^

根据此规则,图中对面的树表示为:afg^^c^bd^e^^^

编写函数 string_of_tree : char mult_tree -> string 来构建表示树的字符串,以及 tree_of_string : string -> char mult_tree 来构建树,当给出字符串时。

# let t = T ('a', [T ('f', [T ('g', [])]); T ('c', []);
          T ('b', [T ('d', []); T ('e', [])])]);;
val t : char mult_tree =
  T ('a',
   [T ('f', [T ('g', [])]); T ('c', []); T ('b', [T ('d', []); T ('e', [])])])
# (* We could build the final string by string concatenation but
     this is expensive due to the number of operations.  We use a
     buffer instead. *)
  let rec add_string_of_tree buf (T (c, sub)) =
    Buffer.add_char buf c;
    List.iter (add_string_of_tree buf) sub;
    Buffer.add_char buf '^'
  let string_of_tree t =
    let buf = Buffer.create 128 in
    add_string_of_tree buf t;
    Buffer.contents buf;;
val add_string_of_tree : Buffer.t -> char mult_tree -> unit = <fun>
val string_of_tree : char mult_tree -> string = <fun>

统计多路树的节点数

初级
# count_nodes (T ('a', [T ('f', []) ]));;
- : int = 2
# let rec count_nodes (T (_, sub)) =
    List.fold_left (fun n t -> n + count_nodes t) 1 sub;;
val count_nodes : 'a mult_tree -> int = <fun>

确定树的内部路径长度

初级

我们将多路树的内部路径长度定义为从根到树中所有节点的路径长度的总和。根据此定义,前一个问题图中的树 t 的内部路径长度为 9。编写一个函数 ipl tree 来返回 tree 的内部路径长度。

# ipl t;;
- : int = 9
# let rec ipl_sub len (T(_, sub)) =
    (* [len] is the distance of the current node to the root.  Add the
       distance of all sub-nodes. *)
    List.fold_left (fun sum t -> sum + ipl_sub (len + 1) t) len sub
  let ipl t = ipl_sub 0 t;;
val ipl_sub : int -> 'a mult_tree -> int = <fun>
val ipl : 'a mult_tree -> int = <fun>

构建树节点的底部向上顺序序列

初级

编写一个函数 bottom_up t,它构建多路树 t 的节点的底部向上序列。

# bottom_up (T ('a', [T ('b', [])]));;
- : char list = ['b'; 'a']
# bottom_up t;;
- : char list = ['g'; 'f'; 'c'; 'd'; 'e'; 'b'; 'a']
# let rec prepend_bottom_up (T (c, sub)) l =
    List.fold_right (fun t l -> prepend_bottom_up t l) sub (c :: l)
  let bottom_up t = prepend_bottom_up t [];;
val prepend_bottom_up : 'a mult_tree -> 'a list -> 'a list = <fun>
val bottom_up : 'a mult_tree -> 'a list = <fun>

类 Lisp 树表示

中级

Lisp 中有一种特殊的多路树表示法。图片显示了多路树结构在 Lisp 中是如何表示的。

Lisp representation of trees

请注意,在“Lisp 式”表示法中,树中具有后继(子节点)的节点始终是列表中的第一个元素,后面跟着它的子节点。“Lisp 式”多路树表示是一个原子和括号 '(' 和 ')' 的序列。这非常接近 OCaml 中树的表示方式,只是没有使用构造函数 T。编写一个函数 lispy : char mult_tree -> string 来返回树的 Lisp 式表示法。

# lispy (T ('a', []));;
- : string = "a"
# lispy (T ('a', [T ('b', [])]));;
- : string = "(a b)"
# lispy t;;
- : string = "(a (f g) c (b d e))"
# let rec add_lispy buf = function
    | T(c, []) -> Buffer.add_char buf c
    | T(c, sub) ->
       Buffer.add_char buf '(';
       Buffer.add_char buf c;
       List.iter (fun t -> Buffer.add_char buf ' '; add_lispy buf t) sub;
       Buffer.add_char buf ')'
  let lispy t =
    let buf = Buffer.create 128 in
    add_lispy buf t;
    Buffer.contents buf;;
val add_lispy : Buffer.t -> char mult_tree -> unit = <fun>
val lispy : char mult_tree -> string = <fun>

转换

初级

A graph

图定义为一组节点和一组边,其中每条边都是一对不同的节点。

在 OCaml 中有几种表示图的方法。

  • 一种方法是列出所有边,一条边是一对节点。在此形式中,上面描述的图表示为以下表达式
# [('h', 'g'); ('k', 'f'); ('f', 'b'); ('f', 'c'); ('c', 'b')];;
- : (char * char) list =
[('h', 'g'); ('k', 'f'); ('f', 'b'); ('f', 'c'); ('c', 'b')]

我们称之为**边子句形式**。显然,孤立节点无法表示。

  • 另一种方法是将整个图表示为一个数据对象。根据图的定义(两组的组合,节点和边),我们可以使用以下 OCaml 类型
# type 'a graph_term = {nodes : 'a list;  edges : ('a * 'a) list};;
type 'a graph_term = { nodes : 'a list; edges : ('a * 'a) list; }

然后,上面的示例图表示为

# let example_graph =
  {nodes = ['b'; 'c'; 'd'; 'f'; 'g'; 'h'; 'k'];
   edges = [('h', 'g'); ('k', 'f'); ('f', 'b'); ('f', 'c'); ('c', 'b')]};;
val example_graph : char graph_term =
  {nodes = ['b'; 'c'; 'd'; 'f'; 'g'; 'h'; 'k'];
   edges = [('h', 'g'); ('k', 'f'); ('f', 'b'); ('f', 'c'); ('c', 'b')]}

我们称之为**图项形式**。请注意,列表保持排序,它们实际上是集合,没有重复元素。每条边在边列表中只出现一次;即,从节点 x 到另一个节点 y 的边表示为 (x, y),元组 (y, x) 不存在。**图项形式是我们默认的表示形式。**你可能想要使用集合而不是列表来定义类似的类型。

  • 第三种表示方法是将与每个节点关联的节点集与每个节点关联。我们称之为**邻接表形式**。在我们的示例中
let adjacency_example = ['b', ['c'; 'f'];
                         'c', ['b'; 'f'];
                         'd', [];
                         'f', ['b'; 'c'; 'k'];
                         'g', ['h'];
                         'k', ['f']
                         ];;
val adjacency_example : (char * char list) list =
  [('b', ['c'; 'f']); ('c', ['b'; 'f']); ('d', []); ('f', ['b'; 'c'; 'k']);
   ('g', ['h']); ('k', ['f'])]
  • 到目前为止,我们介绍的表示方法非常适合自动处理,但它们的语法不是非常友好。手动键入项很麻烦,而且容易出错。我们可以定义一个更紧凑、更“人性化”的表示法,如下所示:图(带有字符标记的节点)由原子和类型为 X-Y 的项的字符串表示。原子代表孤立节点,X-Y 项描述边。如果 X 作为边的端点出现,则它会自动定义为节点。我们的示例可以写成
# "b-c f-c g-h d f-b k-f h-g";;
- : string = "b-c f-c g-h d f-b k-f h-g"

我们称之为**人性化形式**。如示例所示,列表不必排序,甚至可以包含相同的边多次。注意孤立节点 d

编写函数在不同的图表示之间进行转换。使用这些函数,所有表示都是等效的;即,对于以下问题,你可以始终自由地选择最方便的形式。这个问题并不特别困难,但处理所有特殊情况需要大量工作。

(* example pending *)

从一个节点到另一个节点的路径

中级

编写一个函数 paths g a b,它返回图 g 中从节点 a 到节点 b ≠ a 的所有无环路径 p。该函数应该通过回溯返回所有路径的列表。

# let example_graph =
  {nodes = ['b'; 'c'; 'd'; 'f'; 'g'; 'h'; 'k'];
   edges = [('h', 'g'); ('k', 'f'); ('f', 'b'); ('f', 'c'); ('c', 'b')]};;
val example_graph : char graph_term =
  {nodes = ['b'; 'c'; 'd'; 'f'; 'g'; 'h'; 'k'];
   edges = [('h', 'g'); ('k', 'f'); ('f', 'b'); ('f', 'c'); ('c', 'b')]}
# paths example_graph 'f' 'b';;
- : char list list = [['f'; 'c'; 'b']; ['f'; 'b']]
# (* The datastructures used here are far from the most efficient ones
     but allow for a straightforward implementation. *)
  (* Returns all neighbors satisfying the condition. *)
  let neighbors g a cond =
    let edge l (b, c) = if b = a && cond c then c :: l
                        else if c = a && cond b then b :: l
                        else l in
    List.fold_left edge [] g.edges
  let rec list_path g a to_b = match to_b with
    | [] -> assert false (* [to_b] contains the path to [b]. *)
    | a' :: _ ->
       if a' = a then [to_b]
       else
         let n = neighbors g a' (fun c -> not (List.mem c to_b)) in
           List.concat_map (fun c -> list_path g a (c :: to_b)) n

  let paths g a b =
    assert(a <> b);
    list_path g a [b];;
val neighbors : 'a graph_term -> 'a -> ('a -> bool) -> 'a list = <fun>
val list_path : 'a graph_term -> 'a -> 'a list -> 'a list list = <fun>
val paths : 'a graph_term -> 'a -> 'a -> 'a list list = <fun>

从给定节点开始的循环

初级

编写一个函数 cycle g a,它返回从图 g 中给定节点 a 开始的闭合路径(循环)p。谓词应该通过回溯返回所有循环的列表。

# let example_graph =
  {nodes = ['b'; 'c'; 'd'; 'f'; 'g'; 'h'; 'k'];
   edges = [('h', 'g'); ('k', 'f'); ('f', 'b'); ('f', 'c'); ('c', 'b')]};;
val example_graph : char graph_term =
  {nodes = ['b'; 'c'; 'd'; 'f'; 'g'; 'h'; 'k'];
   edges = [('h', 'g'); ('k', 'f'); ('f', 'b'); ('f', 'c'); ('c', 'b')]}
# cycles example_graph 'f';;
- : char list list =
[['f'; 'b'; 'c'; 'f']; ['f'; 'c'; 'f']; ['f'; 'c'; 'b'; 'f'];
 ['f'; 'b'; 'f']; ['f'; 'k'; 'f']]
# let cycles g a =
    let n = neighbors g a (fun _ -> true) in
    let p = List.concat_map (fun c -> list_path g a [c]) n in
    List.map (fun p -> p @ [a]) p;;
val cycles : 'a graph_term -> 'a -> 'a list list = <fun>

构建所有生成树

中级

Spanning tree graph

编写一个函数 s_tree g,通过回溯构建给定图 g 的所有 生成树。使用此谓词,找出左侧描述的图有多少个生成树。此示例图的数据可以在下面的测试中找到。当你对 s_tree 函数有了正确的解决方案后,使用它来定义另外两个有用的函数:is_tree graphis_connected Graph。两者都是五分钟的任务!

# let g = {nodes = ['a'; 'b'; 'c'; 'd'; 'e'; 'f'; 'g'; 'h'];
         edges = [('a', 'b'); ('a', 'd'); ('b', 'c'); ('b', 'e');
                  ('c', 'e'); ('d', 'e'); ('d', 'f'); ('d', 'g');
                  ('e', 'h'); ('f', 'g'); ('g', 'h')]};;
val g : char graph_term =
  {nodes = ['a'; 'b'; 'c'; 'd'; 'e'; 'f'; 'g'; 'h'];
   edges =
    [('a', 'b'); ('a', 'd'); ('b', 'c'); ('b', 'e'); ('c', 'e'); ('d', 'e');
     ('d', 'f'); ('d', 'g'); ('e', 'h'); ('f', 'g'); ('g', 'h')]}
(* solution pending *);;

构建最小生成树

中级

Spanning tree graph

编写一个函数 ms_tree graph 来构建给定标记图的最小生成树。标记图将表示如下

# type ('a, 'b) labeled_graph = {nodes : 'a list;
                               labeled_edges : ('a * 'a * 'b) list};;
type ('a, 'b) labeled_graph = {
  nodes : 'a list;
  labeled_edges : ('a * 'a * 'b) list;
}

(注意,从现在开始 nodesedges 会掩盖相同名称的先前字段。)

提示:使用 普里姆算法。对 P83 解决方案的少量修改就可以解决问题。右侧示例图的数据可以在下面找到。

# let g = {nodes = ['a'; 'b'; 'c'; 'd'; 'e'; 'f'; 'g'; 'h'];
         labeled_edges = [('a', 'b', 5); ('a', 'd', 3); ('b', 'c', 2);
                          ('b', 'e', 4); ('c', 'e', 6); ('d', 'e', 7);
                          ('d', 'f', 4); ('d', 'g', 3); ('e', 'h', 5);
                          ('f', 'g', 4); ('g', 'h', 1)]};;
val g : (char, int) labeled_graph =
  {nodes = ['a'; 'b'; 'c'; 'd'; 'e'; 'f'; 'g'; 'h'];
   labeled_edges =
    [('a', 'b', 5); ('a', 'd', 3); ('b', 'c', 2); ('b', 'e', 4);
     ('c', 'e', 6); ('d', 'e', 7); ('d', 'f', 4); ('d', 'g', 3);
     ('e', 'h', 5); ('f', 'g', 4); ('g', 'h', 1)]}
(* solution pending *);;

图同构

中级

如果存在双射 f: N1 → N2,使得对于 N1 的任何节点 X,Y,X 和 Y 相邻当且仅当 f(X) 和 f(Y) 相邻,那么两个图 G1(N1,E1) 和 G2(N2,E2) 同构。

编写一个函数来确定两个图是否同构。

提示:使用开放式列表来表示函数 f。

# let g = {nodes = [1; 2; 3; 4; 5; 6; 7; 8];
         edges = [(1, 5); (1, 6); (1, 7); (2, 5); (2, 6); (2, 8); (3, 5);
                  (3, 7); (3, 8); (4, 6); (4, 7); (4, 8)]};;
val g : int graph_term =
  {nodes = [1; 2; 3; 4; 5; 6; 7; 8];
   edges =
    [(1, 5); (1, 6); (1, 7); (2, 5); (2, 6); (2, 8); (3, 5); (3, 7);
     (3, 8); (4, 6); (4, 7); (4, 8)]}
(* solution pending *);;

节点度数和图着色

中级
  • 编写一个函数 degree graph node,它确定给定节点的度数。
  • 编写一个函数,它生成一个图中所有节点的列表,根据降序度数排序。
  • 使用 韦尔什-鲍威尔算法 以这样的方式对图的节点进行着色,使相邻节点具有不同的颜色。
(* example pending *);;

深度优先顺序图遍历

中级

编写一个函数,它生成 深度优先顺序图遍历 序列。起点应该指定,输出应该是从该起点可达的节点列表(深度优先顺序)。

具体来说,图将由它的 邻接表表示 提供,你必须创建一个具有以下签名的模块 M

# module type GRAPH = sig
    type node = char
    type t
    val of_adjacency : (node * node list) list -> t
    val dfs_fold : t -> node -> ('a -> node -> 'a) -> 'a -> 'a
  end;;
module type GRAPH =
  sig
    type node = char
    type t
    val of_adjacency : (node * node list) list -> t
    val dfs_fold : t -> node -> ('a -> node -> 'a) -> 'a -> 'a
  end

其中 M.dfs_fold g n f af 应用于图 g 中的节点,深度优先顺序,从节点 n 开始。

# let g = M.of_adjacency
          ['u', ['v'; 'x'];
           'v',      ['y'];
           'w', ['z'; 'y'];
           'x',      ['v'];
           'y',      ['x'];
           'z',      ['z'];
          ];;
val g : M.t = <abstr>

在深度优先搜索中,你需要完全探索最近发现的节点 v 的边,然后“回溯”以探索离开 v 被发现的节点的边。进行深度优先搜索意味着要仔细跟踪哪些顶点已被访问以及何时访问。

我们计算在搜索中发现的每个顶点的 时间戳。一个被发现的顶点有两个时间戳与之关联:它的发现时间(在映射 d 中)和它的完成时间(在映射 f 中)(当顶点的邻接表被完全检查时,顶点就完成了)。这些时间戳通常在图算法中很有用,并有助于推理深度优先搜索的行为。

我们在搜索过程中对节点进行着色以帮助簿记(映射 color)。图的所有顶点最初都为 White。当一个顶点被发现时,它被标记为 Gray,当它完成时,它被标记为 Black

如果顶点 v 在先前发现的节点 u 的邻接表中被发现,则该事实将在前驱子图(映射 pred)中记录下来。

# module M : GRAPH = struct

    module Char_map = Map.Make (Char)
    type node = char
    type t = (node list) Char_map.t

    let of_adjacency l = 
      List.fold_right (fun (x, y) -> Char_map.add x y) l Char_map.empty

    type colors = White|Gray|Black

    type 'a state = {
        d : int Char_map.t; (*discovery time*)
      f : int Char_map.t; (*finishing time*)
      pred : char Char_map.t; (*predecessor*)
      color : colors Char_map.t; (*vertex colors*)
      acc : 'a; (*user specified type used by 'fold'*)
    }

    let dfs_fold g c fn acc =
      let rec dfs_visit t u {d; f; pred; color; acc} =
        let edge (t, state) v =
          if Char_map.find v state.color = White then
            dfs_visit t v {state with pred = Char_map.add v u state.pred}
          else  (t, state)
        in
        let t, {d; f; pred; color; acc} =
          let t = t + 1 in
          List.fold_left edge
            (t, {d = Char_map.add u t d; f;
                 pred; color = Char_map.add u Gray color; acc = fn acc u})
            (Char_map.find u g)
        in
        let t = t + 1 in
        t , {d; f = Char_map.add u t f; pred;
             color = Char_map.add u Black color; acc}
      in
      let v = List.fold_left (fun k (x, _) -> x :: k) []
                             (Char_map.bindings g) in
      let initial_state= 
        {d = Char_map.empty;
         f = Char_map.empty;
         pred = Char_map.empty;
         color = List.fold_right (fun x -> Char_map.add x White)
                                 v Char_map.empty;
         acc}
      in
      (snd (dfs_visit 0 c initial_state)).acc
  end;;
module M : GRAPH

连通分量

中级

编写一个谓词,它将图拆分为其 连通分量

(* example pending *);;

二部图

中级

编写一个谓词来判断给定的图是否为二分图

(* example pending *);;

生成具有 N 个节点的 K-正则简单图

高级

K-正则图中,所有节点的度数均为 K;即,每个节点的入射边数为 K。有多少个(非同构!)具有 6 个节点的 3-正则图?

另请参见结果表

(* example pending *);;

八皇后问题

中级

这是一个经典的计算机科学问题。目标是在棋盘上放置八个皇后,使得任何两个皇后都不会互相攻击;即,没有两个皇后在同一行、同一列或同一对角线上。

提示:将皇后的位置表示为一个 1..N 数字列表。例如:[4; 2; 7; 3; 6; 8; 5; 1] 表示第一列的皇后在第 4 行,第二列的皇后在第 2 行,等等。使用生成-测试范式。

# queens_positions 4;;
- : int list list = [[3; 1; 4; 2]; [2; 4; 1; 3]]

这是一个枚举所有可能解的暴力算法。对于更深入的分析,例如查看维基百科

# let possible row col used_rows usedD1 usedD2 =
    not (List.mem row used_rows
         || List.mem (row + col) usedD1
         || List.mem (row - col) usedD2)
         let queens_positions n =
    let rec aux row col used_rows usedD1 usedD2 =
      if col > n then [List.rev used_rows]
      else
        (if row < n then aux (row + 1) col used_rows usedD1 usedD2
         else [])
        @ (if possible row col used_rows usedD1 usedD2 then
             aux 1 (col + 1) (row :: used_rows) (row + col :: usedD1)
                 (row - col :: usedD2)
           else [])
    in aux 1 1 [] [] [];;
val possible : int -> int -> int list -> int list -> int list -> bool = <fun>
val queens_positions : int -> int list list = <fun>

骑士之旅

中级

另一个著名的难题是:骑士如何在一个 N×N 的棋盘上跳跃,以便正好访问每个方格一次?

提示:用坐标对(x,y)表示方格,其中xy都是 1 到 N 之间的整数。定义函数jump n (x,y),它返回所有坐标(u,v),骑士可以在n×n棋盘上从(x,y)跳到这些坐标。最后,将我们问题的解表示为一个骑士位置列表(骑士之旅)。

(* example pending *);;

冯·科赫猜想

高级

几年前,我遇到了一位对一个他不知道解法的难题着迷的数学家。他的名字是冯·科赫,我不知道这个问题自那时起是否已经得到解决。

Tree numbering

无论如何,谜题是这样的:给定一个有 N 个节点(因此有 N-1 条边)的树。找到一种方法,将节点从 1 到 N 进行编号,并相应地将边从 1 到 N-1 进行编号,使得对于每条边 K,其节点编号之差等于 K。猜想是,这始终是可能的。

对于小的树,这个问题很容易用手解决。但是,对于较大的树,14 已经很大,找到解非常困难。请记住,我们不能确定一定有解!

Larger tree

编写一个函数来计算给定树的编号方案。较大的树的解决方案是什么?

(* example pending *);;

算术谜题

高级

给定一个整数列表,找到一种正确的插入算术符号(运算符)的方法,使得结果为一个正确的等式。例如:对于数字列表[2; 3; 5; 7; 11],我们可以形成等式 2 - 3 + 5 + 7 = 11 或 2 = (3 * 5 + 7) / 11(以及其他十个!)。

(* example pending *);;

英文数字词

中级

在支票等财务文件中,有时必须将数字用完整的单词写出来。例如:175 必须写成 one-seven-five。编写一个函数full_words来打印(非负)整数的完整单词。

# full_words 175;;
- : string = "one-seven-five"
# let full_words =
    let digit = [|"zero"; "one"; "two"; "three"; "four"; "five"; "six";
                  "seven"; "eight"; "nine"|] in
    let rec words w n =
      if n = 0 then (match w with [] -> [digit.(0)] | _ -> w)
      else words (digit.(n mod 10) :: w) (n / 10)
    in
      fun n -> String.concat "-" (words [] n);;
val full_words : int -> string = <fun>

语法检查器

中级

Syntax graph

在某种编程语言(Ada)中,标识符由相反的语法图(铁路图)定义。将语法图转换为一个不包含循环的语法图系统;即,纯粹是递归的。使用这些修改后的图,编写一个函数identifier : string -> bool,它可以检查给定的字符串是否为合法标识符。

# identifier "this-is-a-long-identifier";;
- : bool = true
# let identifier =
    let is_letter c = 'a' <= c && c <= 'z' in
    let is_letter_or_digit c = is_letter c || ('0' <= c && c <= '9') in
    let rec is_valid s i not_after_dash =
      if i < 0 then not_after_dash
      else if is_letter_or_digit s.[i] then is_valid s (i - 1) true
      else if s.[i] = '-' && not_after_dash then is_valid s (i - 1) false
      else false in
    fun s -> (
        let n = String.length s in
      n > 0 && is_letter s.[n - 1] && is_valid s (n - 2) true);;
val identifier : string -> bool = <fun>

数独

中级

数独谜题是这样的

   Problem statement                 Solution

    .  .  4 | 8  .  . | .  1  7      9  3  4 | 8  2  5 | 6  1  7
            |         |                      |         |
    6  7  . | 9  .  . | .  .  .      6  7  2 | 9  1  4 | 8  5  3
            |         |                      |         |
    5  .  8 | .  3  . | .  .  4      5  1  8 | 6  3  7 | 9  2  4
    --------+---------+--------      --------+---------+--------
    3  .  . | 7  4  . | 1  .  .      3  2  5 | 7  4  8 | 1  6  9
            |         |                      |         |
    .  6  9 | .  .  . | 7  8  .      4  6  9 | 1  5  3 | 7  8  2
            |         |                      |         |
    .  .  1 | .  6  9 | .  .  5      7  8  1 | 2  6  9 | 4  3  5
    --------+---------+--------      --------+---------+--------
    1  .  . | .  8  . | 3  .  6      1  9  7 | 5  8  2 | 3  4  6
            |         |                      |         |
    .  .  . | .  .  6 | .  9  1      8  5  3 | 4  7  6 | 2  9  1
            |         |                      |         |
    2  4  . | .  .  1 | 5  .  .      2  4  6 | 3  9  1 | 5  7  8

谜题中的每个位置都属于一个(水平)行和一个(垂直)列,以及一个唯一的 3x3 方格(我们简称“方格”)。在开始时,一些位置带有 1 到 9 之间的单个数字。问题是要用数字填充缺失的位置,使得每个数字从 1 到 9 在每一行、每一列和每个方格中恰好出现一次。

# (* The board representation is not imposed.  Here "0" stands for "." *);;

解决这个问题的一个简单方法是使用暴力破解。思路是在每种情况下用可用值开始填充,并测试它是否有效。当没有可用值时,这意味着我们犯了一个错误,因此我们回到上一次的选择,并尝试不同的选择。

# open Printf

  module Board = struct
    type t = int array (* 9×9, row-major representation.  A value of 0
                          means undecided. *)

    let is_valid c = c >= 1

    let get (b : t) (x, y) = b.(x + y * 9)

    let get_as_string (b : t) pos =
      let i = get b pos in
      if is_valid i then string_of_int i else "."

    let with_val (b : t) (x, y) v =
      let b = Array.copy b in
      b.(x + y * 9) <- v;
      b

    let of_list l : t =
      let b = Array.make 81 0 in
      List.iteri (fun y r -> List.iteri (fun x e ->
        b.(x + y * 9) <- if e >= 0 && e <= 9 then e else 0) r) l;
      b

    let print b =
      for y = 0 to 8 do
        for x = 0 to 8 do
          printf (if x = 0 then "%s" else if x mod 3 = 0 then " | %s"
                  else "  %s")  (get_as_string b (x, y))
        done;
        if y < 8 then
          if y mod 3 = 2 then printf "\n--------+---------+--------\n"
          else printf "\n        |         |        \n"
        else printf "\n"
      done

    let available b (x, y) =
      let avail = Array.make 10 true in
      for i = 0 to 8 do
        avail.(get b (x, i)) <- false;
        avail.(get b (i, y)) <- false;
      done;
      let sq_x = x - x mod 3 and sq_y = y - y mod 3 in
      for x = sq_x to sq_x + 2 do
        for y = sq_y to sq_y + 2 do
          avail.(get b (x, y)) <- false;
        done;
      done;
      let av = ref [] in
      for i = 1 (* not 0 *) to 9 do if avail.(i) then av := i :: !av done;
      !av

    let next (x,y) = if x < 8 then (x + 1, y) else (0, y + 1)

    (** Try to fill the undecided entries. *)
    let rec fill b ((x, y) as pos) =
      if y > 8 then Some b (* filled all entries *)
      else if is_valid(get b pos) then fill b (next pos)
      else match available b pos with
           | [] -> None (* no solution *)
           | l -> try_values b pos l
    and try_values b pos = function
      | v :: l ->
         (match fill (with_val b pos v) (next pos) with
          | Some _ as res -> res
          | None -> try_values b pos l)
      | [] -> None
  end

  let sudoku b = match Board.fill b (0, 0) with
    | Some b -> b
    | None -> failwith "sudoku: no solution";;
module Board :
  sig
    type t = int array
    val is_valid : int -> bool
    val get : t -> int * int -> int
    val get_as_string : t -> int * int -> string
    val with_val : t -> int * int -> int -> int array
    val of_list : int list list -> t
    val print : t -> unit
    val available : t -> int * int -> int list
    val next : int * int -> int * int
    val fill : t -> int * int -> t option
    val try_values : t -> int * int -> int list -> t option
  end
val sudoku : Board.t -> Board.t = <fun>

非ograms

高级

大约 1994 年,一种特定的谜题在英国非常流行。“星期日电讯报”报纸写道:“非ograms 是一种来自日本的谜题,目前仅在《星期日电讯报》上每周出版一次。只需使用您的逻辑和技巧来完成网格,并揭示图片或图表。”作为一名 OCaml 程序员,你的处境要好得多:你可以让你的计算机完成工作!

谜题是这样的:从本质上讲,矩形位图的每一行和每一列都用其各自的已占用单元格的独立字符串的长度进行注释。解决谜题的人必须仅凭这些长度来完成位图。

          Problem statement:          Solution:

          |_|_|_|_|_|_|_|_| 3         |_|X|X|X|_|_|_|_| 3
          |_|_|_|_|_|_|_|_| 2 1       |X|X|_|X|_|_|_|_| 2 1
          |_|_|_|_|_|_|_|_| 3 2       |_|X|X|X|_|_|X|X| 3 2
          |_|_|_|_|_|_|_|_| 2 2       |_|_|X|X|_|_|X|X| 2 2
          |_|_|_|_|_|_|_|_| 6         |_|_|X|X|X|X|X|X| 6
          |_|_|_|_|_|_|_|_| 1 5       |X|_|X|X|X|X|X|_| 1 5
          |_|_|_|_|_|_|_|_| 6         |X|X|X|X|X|X|_|_| 6
          |_|_|_|_|_|_|_|_| 1         |_|_|_|_|X|_|_|_| 1
          |_|_|_|_|_|_|_|_| 2         |_|_|_|X|X|_|_|_| 2
           1 3 1 7 5 3 4 3             1 3 1 7 5 3 4 3
           2 1 5 1                     2 1 5 1

对于上面的示例,问题可以表述为两个列表[[3]; [2; 1]; [3; 2]; [2; 2]; [6]; [1; 5]; [6]; [1]; [2]][[1; 2]; [3; 1]; [1; 5]; [7; 1]; [5]; [3]; [4]; [3]],它们分别给出行和列的“实心”长度,从上到下和从左到右。出版的谜题比这个例子更大,例如 25×20,而且显然总是只有唯一的解。

# solve [[3]; [2; 1]; [3; 2]; [2; 2]; [6]; [1; 5]; [6]; [1]; [2]]
      [[1; 2]; [3; 1]; [1; 5]; [7; 1]; [5]; [3]; [4]; [3]];;

暴力破解解决方案:构建棋盘,尝试所有针对列的填充可能性,以满足为它们指定的模式,并在不满足行模式的情况下拒绝解决方案。

# type element = Empty | X (* ensure we do not miss cases in patterns *);;
type element = Empty | X

你可能想查看更有效的算法并实现它们,这样你就可以在合理的时间内解决以下问题

  solve [[14]; [1; 1]; [7; 1]; [3; 3]; [2; 3; 2];
         [2; 3; 2]; [1; 3; 6; 1; 1]; [1; 8; 2; 1]; [1; 4; 6; 1]; [1; 3; 2; 5; 1; 1];
         [1; 5; 1]; [2; 2]; [2; 1; 1; 1; 2]; [6; 5; 3]; [12]]
        [[7]; [2; 2]; [2; 2]; [2; 1; 1; 1; 1]; [1; 2; 4; 2];
         [1; 1; 4; 2]; [1; 1; 2; 3]; [1; 1; 3; 2]; [1; 1; 1; 2; 2; 1]; [1; 1; 5; 1; 2];
         [1; 1; 7; 2]; [1; 6; 3]; [1; 1; 3; 2]; [1; 4; 3]; [1; 3; 1];
         [1; 2; 2]; [2; 1; 1; 1; 1]; [2; 2]; [2; 2]; [7]]

纵横字谜

高级

Crossword

给定一个空的(或几乎空的)纵横字谜框架和一组单词。问题是将这些单词放置到框架中。

特定的纵横字谜在文本文件中指定,该文件首先列出单词(每行一个单词),以任意顺序。然后,在空行之后,定义纵横字谜框架。在这个框架规范中,空字符位置用点(.)表示。为了使解决方案更容易,字符位置也可以包含预定义的字符值。上面的谜题在文件p7_09a.dat中定义,其他示例是p7_09b.datp7_09d.dat。还有一个没有解决方案的谜题示例(p7_09c.dat)。

单词是字符串(字符列表),至少包含两个字符。纵横字谜框架中水平或垂直的字符位置序列称为站点。我们的问题是找到一种兼容的方式将单词放置到站点上。

提示

  1. 这个问题并不容易。你需要一些时间才能彻底理解它。所以,不要过早放弃!请记住,目标是一个干净的解决方案,而不仅仅是一个快速而肮脏的黑客!
  2. 出于效率原因,对于较大的谜题,对单词和站点进行排序很重要。
(* example pending *);;

永不结束的序列

初级

列表是有限的,这意味着它们总是包含有限数量的元素。序列可以是有限的或无限的。

本练习的目标是定义一个类型'a stream,它只包含无限序列。使用此类型,定义以下函数

val hd : 'a stream -> 'a
(** Returns the first element of a stream *)
val tl : 'a stream -> 'a stream
(** Removes the first element of a stream *)
val take : int -> 'a stream -> 'a list
(** [take n seq] returns the n first values of [seq] *)
val unfold : ('a -> 'b * 'a) -> 'a -> 'b stream
(** Similar to Seq.unfold *)
val bang : 'a -> 'a stream
(** [bang x] produces an infinitely repeating sequence of [x] values. *)
val ints : int -> int stream
(* Similar to Seq.ints *)
val map : ('a -> 'b) -> 'a stream -> 'b stream
(** Similar to List.map and Seq.map *)
val filter: ('a -> bool) -> 'a stream -> 'a stream
(** Similar to List.filter and Seq.filter *)
val iter : ('a -> unit) -> 'a stream -> 'b
(** Similar to List.iter and Seq.iter *)
val to_seq : 'a stream -> 'a Seq.t
(** Translates an ['a stream] into an ['a Seq.t] *)
val of_seq : 'a Seq.t -> 'a stream
(** Translates an ['a Seq.t] into an ['a stream]
    @raise Failure if the input sequence is finite. *)

提示:使用let ... =模式。

type 'a cons = Cons of 'a * 'a stream
and 'a stream = unit -> 'a cons

let hd (seq : 'a stream) = let (Cons (x, _)) = seq () in x
let tl (seq : 'a stream) = let (Cons (_, seq)) = seq () in seq
let rec take n seq = if n = 0 then [] else let (Cons (x, seq)) = seq () in x :: take (n - 1) seq
let rec unfold f x () = let (y, x) = f x in Cons (y, unfold f x)
let bang x = unfold (fun x -> (x, x)) x
let ints x = unfold (fun x -> (x, x + 1)) x
let rec map f seq () = let (Cons (x, seq)) = seq () in Cons (f x, map f seq)
let rec filter p seq () = let (Cons (x, seq)) = seq () in let seq = filter p seq in if p x then Cons (x, seq) else seq ()
let rec iter f seq = let (Cons (x, seq)) = seq () in f x; iter f seq
let to_seq seq = Seq.unfold (fun seq -> Some (hd seq, tl seq)) seq
let rec of_seq seq () = match seq () with
| Seq.Nil -> failwith "Not a infinite sequence"
| Seq.Cons (x, seq) -> Cons (x, of_seq seq)

序列序列的对角线

中级

编写一个函数diag : 'a Seq.t Seq.t -> 'a Seq,它返回序列序列的对角线。返回的序列按如下方式形成:返回序列的第一个元素是第一个序列的第一个元素;返回序列的第二个元素是第二个序列的第二个元素;返回序列的第三个元素是第三个序列的第三个元素;依此类推。

let rec diag seq_seq () =
    let hds, tls = Seq.filter_map Seq.uncons seq_seq |> Seq.split in
    let hd, tl = Seq.uncons hds |> Option.map fst, Seq.uncons tls |> Option.map snd in
    let d = Option.fold ~none:Seq.empty ~some:diag tl in
    Option.fold ~none:Fun.id ~some:Seq.cons hd d ()
OCaml

创新。社区。安全。