Counting Triangles in Golang

序言

正月十五是一年一度的元宵節(jié),是春節(jié)之后的第一個(gè)重要節(jié)日。在我們中國(guó)人眼里,過完正月十五元宵節(jié),這年才算是真正地過完了。猜燈謎、賞花燈、舞龍獅、吃元宵、看晚會(huì),老百姓把元宵節(jié)過得有滋有味,熱鬧非凡。還值得一提的是,古城西安今年在元宵節(jié)推出了“西安年·最中國(guó)”的壓軸大戲,即500架無人機(jī)燈光秀,點(diǎn)亮了元宵節(jié)的璀璨夜空。

除了過元宵節(jié),在朋友圈也看到“猿霄節(jié)“的說法:

猿霄節(jié),是程序猿通宵趕代碼的中國(guó)傳統(tǒng)節(jié)日,在此佳節(jié)來臨之際,恭祝全國(guó)程序猿節(jié)日快樂。

筆者感覺“猿霄節(jié)“非常好玩,就有了即興寫篇文章的想法,于是在過完元宵節(jié)后,找了一個(gè)題目:數(shù)三角形。這個(gè)題目是2015年參加一個(gè)軟件設(shè)計(jì)類培訓(xùn)的練習(xí)題之一,當(dāng)時(shí)是用Erlang語言實(shí)現(xiàn)的,今天打算用Golang實(shí)現(xiàn)一下,并將整個(gè)建模和實(shí)現(xiàn)過程通過文章記錄下來,以便作為“猿霄節(jié)“的一個(gè)禮物送給廣大讀者,尤其是Gophers。

題目

數(shù)數(shù)下圖中一共包含多少個(gè)三角形?

triangle.png

答案是24,你數(shù)對(duì)了嗎?

記得當(dāng)時(shí)參加培訓(xùn)的同學(xué)給出的答案有很多個(gè),有少于24的,也有多于24的,每個(gè)人的數(shù)法不盡相同。但如果讓計(jì)算機(jī)來數(shù)的話,就必須將數(shù)三角形的方法描述成計(jì)算機(jī)可以執(zhí)行的形式化算法。這種描述方法可以有很多種,而我們希望找到一套抽象層次高且非常貼合領(lǐng)域的描述,以便降低后續(xù)的維護(hù)成本。

領(lǐng)域模型設(shè)計(jì)

我們考慮一下:什么是三角形?
如果沒有記錯(cuò)的話,我們?cè)谛W(xué)就學(xué)過:三角形就是三個(gè)點(diǎn),兩兩相連,但是三個(gè)點(diǎn)不同時(shí)在一條直線上。
我們用形式化的方法表達(dá)一下:

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

領(lǐng)域模型設(shè)計(jì)的核心是解決下面兩個(gè)問題:

  • 三角形的三個(gè)點(diǎn)(x, y, z)都有哪些?
  • 計(jì)算機(jī)怎么知道connectedin_same_line?

(x, y, z)的集合

從題目中,我們可以知道點(diǎn)的集合points,不妨用字符串來表示:

points = "abcdefghijk"

(x, y, z)的集合簡(jiǎn)單講就是從points的集合中取三個(gè)點(diǎn)的子集,每個(gè)子集是個(gè)字符串,所有子集就是一個(gè)字符串?dāng)?shù)組切片,可以簡(jiǎn)單用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,不妨用字符串?dāng)?shù)組切片來表示:

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

目標(biāo)是要知道點(diǎn)與點(diǎn)之間關(guān)系:2個(gè)點(diǎn)的關(guān)系connected,3個(gè)點(diǎn)的關(guān)系in_same_line,等價(jià)于2個(gè)點(diǎn)或3個(gè)點(diǎn)組成的集合points是否為lines中任一元素的子集,不妨用belong(points, lines) : (string, []string) -> bool來表示這種關(guān)系,而belong已經(jīng)是一個(gè)原子語義。

我們下面用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)

領(lǐng)域模型實(shí)現(xiàn)

subset的實(shí)現(xiàn):

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的實(shí)現(xiàn):

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的實(shí)現(xiàn):

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

CountingTriangles的實(shí)現(xiàn)中準(zhǔn)確的表達(dá)了領(lǐng)域模型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
}

領(lǐng)域模型應(yīng)用

在應(yīng)用層表達(dá)題目:

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)
}

運(yùn)行main函數(shù):

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]

小結(jié)

領(lǐng)域建模在于不斷的挖掘領(lǐng)域的本質(zhì),然后用優(yōu)秀的代碼簡(jiǎn)潔地表達(dá)領(lǐng)域模型。我們通常習(xí)慣于將所有問題按OO范式的思維來建模,而FP范式非常適合將領(lǐng)域映射到數(shù)學(xué)本質(zhì)上。今后,我們?cè)诮鉀Q特定領(lǐng)域問題時(shí)要選擇最合適的編程范式,從而簡(jiǎn)單高效的解決問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容