Counting Triangles in Golang

序言

正月十五是一年一度的元宵节,是春节之后的第一个重要节日。在我们中国人眼里,过完正月十五元宵节,这年才算是真正地过完了。猜灯谜、赏花灯、舞龙狮、吃元宵、看晚会,老百姓把元宵节过得有滋有味,热闹非凡。还值得一提的是,古城西安今年在元宵节推出了“西安年·最中国”的压轴大戏,即500架无人机灯光秀,点亮了元宵节的璀璨夜空。

除了过元宵节,在朋友圈也看到“猿霄节“的说法:

猿霄节,是程序猿通宵赶代码的中国传统节日,在此佳节来临之际,恭祝全国程序猿节日快乐。

笔者感觉“猿霄节“非常好玩,就有了即兴写篇文章的想法,于是在过完元宵节后,找了一个题目:数三角形。这个题目是2015年参加一个软件设计类培训的练习题之一,当时是用Erlang语言实现的,今天打算用Golang实现一下,并将整个建模和实现过程通过文章记录下来,以便作为“猿霄节“的一个礼物送给广大读者,尤其是Gophers。

题目

数数下图中一共包含多少个三角形?

Counting Triangles in Golang_第1张图片
triangle.png

答案是24,你数对了吗?

记得当时参加培训的同学给出的答案有很多个,有少于24的,也有多于24的,每个人的数法不尽相同。但如果让计算机来数的话,就必须将数三角形的方法描述成计算机可以执行的形式化算法。这种描述方法可以有很多种,而我们希望找到一套抽象层次高且非常贴合领域的描述,以便降低后续的维护成本。

领域模型设计

我们考虑一下:什么是三角形?
如果没有记错的话,我们在小学就学过:三角形就是三个点,两两相连,但是三个点不同时在一条直线上。
我们用形式化的方法表达一下:

is_triangle(x, y, z) ->
    connected(x, y) and 
    connected(y, z) and 
    connected(x, z) and
    not(in_same_line(a, b, c))

领域模型设计的核心是解决下面两个问题:

  • 三角形的三个点(x, y, z)都有哪些?
  • 计算机怎么知道connectedin_same_line

(x, y, z)的集合

从题目中,我们可以知道点的集合points,不妨用字符串来表示:

points = "abcdefghijk"

(x, y, z)的集合简单讲就是从points的集合中取三个点的子集,每个子集是个字符串,所有子集就是一个字符串数组切片,可以简单用subset(points, 3)来描述。不过为了通用,我们直接用subset(points, n) : (string, int) -> []string来描述:

subset(points, n) when len(points) < n -> []

subset(points, n) when len(points) == n -> [points]

subset(points, n) when n == 1 -> [points[0], points[1], ..., points[len(points) - 1]]

subset(points, n) -> 
    firsts = subset(points[1:], n - 1) with points[0] +
    lasts = subset(points[1:], n)
    firsts append lasts

connected和in_same_line

从题目中,可以知道线的集合lines,不妨用字符串数组切片来表示:

lines = {"abh", "acgi", "adfj", "aek", "bcde", "hgfe", "hick"}

目标是要知道点与点之间关系:2个点的关系connected,3个点的关系in_same_line,等价于2个点或3个点组成的集合points是否为lines中任一元素的子集,不妨用belong(points, lines) : (string, []string) -> bool来表示这种关系,而belong已经是一个原子语义。

我们下面用belong描述一下connected(x, y) : (byte, byte) -> boolin_same_line(x, y, z) : (byte, byte, byte) -> bool:

connected(x, y) ->
    belong(xy, lines)

in_same_line(x, y, z) ->
    belong(xyz, lines)

领域模型实现

subset的实现:

func subset(points string, n int) []string {
    l := len(points)
    if l < n {
        return nil
    }
    if l == n {
        return []string{points}
    }

    results := make([]string, 0)
    if n == 1 {
        for i, _ := range points {
            bs := []byte{points[i]}
            results = append(results, string(bs))
        }
        return results
    }

    firsts := subset(points[1:], n - 1)
    for _, first := range firsts {
        results = append(results, string([]byte{points[0]}) + first)
    }

    lasts := subset(points[1:], n)
    results = append(results, lasts...)

    return results
}

belong的实现:

func belong(points string, lines []string) bool {
    flag := false
    for _, line := range lines {
        flag = true
        for _, r := range points {
            if !strings.ContainsRune(line, r) {
                flag = false
                break
            }
        }
        if flag {
            return true
        }
    }
    return false
}

not的实现:

func not(flag bool) bool {
    return !flag
}

CountingTriangles的实现中准确的表达了领域模型is_triangle

func CountingTriangles(points string, lines []string) []string {
    connected := func(x, y byte) bool {
        bs := make([]byte, 2)
        bs[0] = x
        bs[1] = y
        return belong(string(bs), lines)
    }

    in_same_line := func(x, y, z byte) bool {
        bs := make([]byte, 3)
        bs[0] = x
        bs[1] = y
        bs[2] = z
        return belong(string(bs), lines)
    }

    is_triangle := func(x, y, z byte) bool {
        if connected(x, y) &&
            connected(y, z) &&
            connected(x, z) &&
            not(in_same_line(x, y, z)) {
            return true
        }
        return false
    }

    num := 0
    matches := make([]string, 0)
    rs := subset(points, 3)
    for _, r := range rs {
        if len(r) != 3 {
            continue
        }
        if is_triangle(r[0], r[1], r[2]) {
            matches = append(matches, r)
            num++
        }
    }
    return matches
}

领域模型应用

在应用层表达题目:

package main

import (
    "fmt"
    "triangle/domain"
)

func main() {
    points := "abcdefghijk"
    lines := []string{"abh", "acgi", "adfj", "aek", "bcde", "hgfe", "hijk"}
    matches := domain.CountingTriangles(points, lines)
    fmt.Println("counting triangles result: ")
    fmt.Println("num of triangles: ", len(matches))
    fmt.Println("detail of triangles: ", matches)
}

运行main函数:

counting triangles result: 
num of triangles:  24
detail of triangles:  [abc abd abe acd ace ade aef aeg aeh afg afh agh ahi ahj ahk aij aik ajk beh ceg def ehk fhj ghi]

小结

领域建模在于不断的挖掘领域的本质,然后用优秀的代码简洁地表达领域模型。我们通常习惯于将所有问题按OO范式的思维来建模,而FP范式非常适合将领域映射到数学本质上。今后,我们在解决特定领域问题时要选择最合适的编程范式,从而简单高效的解决问题。

你可能感兴趣的:(Counting Triangles in Golang)