問題64

2分木を画像として描画するためのレイアウトを計算する。

2分木を画像として描画する

2分木のレイアウト情報 layout : BinTree (α × (Nat × Nat)) が与えられたときに、その描画情報を SVG 画像として描画する関数をまず実装する。

import LeanBook.Problem63
import ProofWidgets.Data.Svg
import ProofWidgets.Component.HtmlDisplay

open ProofWidgets Svg

variable {α : Type} [ToString α]

structure Config where
  /-- ノードを表現する円の半径 -/
  radius : Nat
  /-- ノードの円の塗りつぶし色 (RGB) -/
  fillColor : Float × Float × Float
  /-- ノードのラベルのフォントサイズ -/
  fontsize : Nat

/-- デフォルトの設定値 -/
private def defaultConfig : Config where
  radius := 16
  fillColor := (0.74, 0.87, 1.0)
  fontsize := 14

/-- 2分木のノードの描画情報 -/
structure NodeData (f : Frame) where
  /-- x座標 -/
  x : Float
  /-- y座標 -/
  y : Float
  /-- 上面と下面を反転して計測したy座標 -/
  yf : Float := f.height.toFloat - y
  /-- ノードのラベル -/
  label : String

def NodeData.ofPair (f : Frame) (pair : α × Nat × Nat) (step : Float) : NodeData f :=
  let (a, x, y) := pair
  { x := x.toFloat * step, y := y.toFloat * step, label := toString a }

private def defaultFrame : Frame where
  xmin := 0
  ymin := 0
  xSize := 1000
  width := 1000
  height := 1000

/-- ノード(円とラベル)を作成する -/
def createNodeElement (f : Frame) (node : NodeData f) (config : Config) : Array (Element f) :=
  let (r, g, b) := config.fillColor
  #[
    circle (node.x, node.yf) (.px config.radius)
    |>.setFill (r, g, b),

    text (node.x - 5, node.yf - 5) node.label (.px config.fontsize)
    |>.setFill (0.0, 0.0, 0.0)
  ]

/-- `createNodeElement` の表示結果をテストするための関数 -/
def createNodeHtml (f := defaultFrame) (node : NodeData f) (config := defaultConfig) : Html :=
  let svg : Svg f := { elements := createNodeElement f node config }
  svg.toHtml

#html createNodeHtml (node := { x := 150, y := 30, label := "A" })

/-- エッジ(ノードの親子関係)を作成する -/
def createEdgeElement (f : Frame) (parent child : NodeData f) : Element f :=
  line (parent.x, parent.yf) (child.x, child.yf)
  |>.setStroke (50., 50., 50.) (.px 2)

/-- `createEdgeElement` の表示結果をテストするための関数 -/
def createEdgeHtml (f := defaultFrame) (parent child : NodeData f) : Html :=
  let svg : Svg f := { elements := #[createEdgeElement f parent child] }
  svg.toHtml

#html createEdgeHtml
  (parent := { x := 150, y := 30, label := "A" })
  (child := { x := 100, y := 80, label := "B" })

/-- 二分木のノードを(木の構造を壊して)リストにする -/
def BinTree.toNodes {β : Type} (tree : BinTree β) : List β :=
  match tree with
  | .empty => []
  | .node a left right => a :: (toNodes left ++ toNodes right)

/-- 二分木のエッジをリストにする。(親, 子) のペアにして返すことに注意。 -/
def BinTree.toEdges {β : Type} (tree : BinTree β) : List (β × β) :=
  match tree with
  | .empty => []
  | .node a left right =>
    let leftEdges :=
      match left with
      | .empty => []
      | .node b _ _ => [(a, b)] ++ toEdges left
    let rightEdges :=
      match right with
      | .empty => []
      | .node c _ _ => [(a, c)] ++ toEdges right
    leftEdges ++ rightEdges

#guard [tree| 1 * (2 + 3 * (4 + ∅))].toEdges = [(1, 2), (1, 3), (3, 4)]

def BinTree.toElementsFromLayout (f : Frame) (config : Config) (tree : BinTree (α × (Nat × Nat))) (step : Float) : Array (Svg.Element f) :=
  let nodes : Array (Element f) := tree.toNodes
    |>.map (NodeData.ofPair f (step := step))
    |>.toArray
    |>.map (fun node => createNodeElement f node config)
    |>.flatten

  let edges := tree.toEdges
    |>.map (fun ((v1, x1, y1), (v2, x2, y2)) => (NodeData.ofPair f (v1, x1, y1) step, NodeData.ofPair f (v2, x2, y2) step))
    |>.map (fun (parent, child) => createEdgeElement f parent child)
    |>.toArray

  edges ++ nodes

/-- 2分木の描画情報が与えられたときに、それを SVG 画像として描画する -/
def BinTree.toHtmlFromLayout (tree : BinTree (α × (Nat × Nat))) (step := 30.0) : Html :=
  let svg : Svg defaultFrame := { elements := tree.toElementsFromLayout defaultFrame defaultConfig step }
  svg.toHtml

#html
  let treeLayout := BinTree.node ("A", (2, 1))
    (.node ("B", (1, 2)) .empty .empty)
    (.node ("C", (4, 2))
      (.node ("D", (3, 3)) .empty .empty)
      (.node ("E", (5, 3)) .empty .empty))
  BinTree.toHtmlFromLayout treeLayout

#html BinTree.toHtmlFromLayout (BinTree.leaf (3, (1, 1)))

回答

/-- 2分木のレイアウト情報が渡されたときに、各ノードのレイアウト位置を一様にずらす -/
def BinTree.shift {β γ : Type} (tree : BinTree (α × β)) (shiftFn : β → γ) : BinTree (α × γ) :=
  match tree with
  | .empty => .empty
  | .node (a, x) left right =>
    let x' := shiftFn x
    .node (a, x') (shift left shiftFn) (shift right shiftFn)

/-- 2分木の幅 -/
def BinTree.width (tree : BinTree α) : Nat :=
  tree.nodes - 1

#guard [tree| 1 * (2 + ∅)].width = 1
#guard [tree| 1 * (2 + 3)].width = 2
#guard [tree| 'c' * ('a' + 'h' * ('g' * ('e' + ∅) + ∅))].width = 4

/-- 2分木に対して、描画情報を付与する関数。各ノードの描画位置を指定している。 -/
def BinTree.layout (t : BinTree α) : BinTree (α × (Nat × Nat)) :=
  match t with
  | .empty => .empty
  | .node a .empty .empty =>
    .node (a, (1, 1)) .empty .empty
  | .node a .empty right =>
    let rightLayout := layout right
    let rightShifted := rightLayout.shift (fun (x, y) => (x + 1, y + 1))
    .node (a, (1, 1)) .empty rightShifted
  | .node a left .empty =>
    let leftLayout := layout left
    let leftShifted := leftLayout.shift (fun (x, y) => (x, y + 1))
    .node (a, left.width + 2, 1) leftShifted .empty
  | .node a left right =>
    let leftLayout := layout left
    let rightLayout := layout right
    let leftShifted := leftLayout.shift (fun (x, y) => (x, y + 1))
    let rightShifted := rightLayout.shift (fun (x, y) => (x + (left.width + 2) * 1, y + 1))
    .node (a, (left.width + 2, 1)) leftShifted rightShifted

#guard
  let actual := (BinTree.layout [tree| 'a' * ('b' + ∅)]).toNodes
  let expected := [
    ('a', (2, 1)),
    ('b', (1, 2))
  ]
  actual = expected

#guard
  let actual := (BinTree.layout [tree| 'a' * ('b' + 'c')]).toNodes
  let expected := [
    ('a', (2, 1)),
    ('b', (1, 2)),
    ('c', (3, 2))
  ]
  actual = expected

#guard
  let actual := (BinTree.layout [tree| 'a' * ('b' * ('c' + ∅) + ∅)]).toNodes
  let expected := [
    ('a', (3, 1)),
    ('b', (2, 2)),
    ('c', (1, 3))
  ]
  actual = expected

#guard
  let actual := (BinTree.layout [tree| 'a' * ('b' * (∅ + 'c') + ∅)]).toNodes
  let expected := [
    ('a', (3, 1)),
    ('b', (1, 2)),
    ('c', (2, 3))
  ]
  actual = expected

#guard
  let actual := (BinTree.layout [tree| 'a' * ('b' * (∅ + 'c') + 'd' * ('e' + ∅))]).toNodes
  let expected := [
    ('a', (3, 1)),
    ('b', (1, 2)),
    ('c', (2, 3)),
    ('d', (5, 2)),
    ('e', (4, 3))
  ]
  actual = expected

#html BinTree.toHtmlFromLayout (BinTree.layout [tree| 1 * (∅ + 2)])
#html BinTree.toHtmlFromLayout (BinTree.layout [tree| 1 * (2 + ∅)])
#html BinTree.toHtmlFromLayout (BinTree.layout [tree| 1 * (2 + 3)])
#html BinTree.toHtmlFromLayout (BinTree.layout [tree| 'a' * ('b' * (∅ + 'c') + ∅)])
#html BinTree.toHtmlFromLayout (BinTree.layout [tree| 1 * (2 * (6 + 7) + 3 * (4 + 5))])
#html BinTree.toHtmlFromLayout (BinTree.layout [tree| 'a' * ('b' * (∅ + 'c') + 'd' * ('e' + ∅))])
#html
  let tree := [tree| 'n' * ('k' * ('c' * ('a' + 'h' * ('g' * ('e' + ∅) + ∅)) + 'm') + 'u' * ('p' * (∅ + 's' * ('q' + ∅)) + ∅))]
  BinTree.toHtmlFromLayout tree.layout