Counting Triangles in Golang

序言

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

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

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

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

题目

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

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范式非常适合将领域映射到数学本质上。今后,我们在解决特定领域问题时要选择最合适的编程范式,从而简单高效的解决问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容