從一個問題中瞭解數學在程式設計中的應用

zangeci發表於2021-10-01

寫在前面

由於這兩天測試機房停電,這兩天無事可做,閒著幹嘛呢?想著要不試著再寫篇文章來嘮嘮吧。於是就有了今天這篇。

題選由來

在上個月,社群裡有人提了這麼一個問題——這JS判斷怎麼簡化,把我給看暈了

const isDel = (op, o) => {
    let fal = false;
    if (op.is_show_lock && op.is_show_sf && op.is_show_bz) {
        if (o.is_lock && o.is_sf && o.is_bz) {
            fal = false;
        } else {
            fal = true;
        }
    } else if (op.is_show_lock && op.is_show_sf) {
        if (o.is_lock && o.is_sf) {
            fal = false;
        } else {
            fal = true;
        }
    } else if (op.is_show_lock && op.is_show_bz) {
        if (o.is_lock && o.is_bz) {
            fal = false;
        } else {
            fal = true;
        }
    } else if (op.is_show_sf && op.is_show_bz) {
        if (o.is_sf && o.is_bz) {
            fal = false;
        } else {
            fal = true;
        }
    } else if (op.is_show_lock) {
        if (o.is_lock) {
            fal = false;
        } else {
            fal = true;
        }
    } else if (op.is_show_sf) {
        if (o.is_sf) {
            fal = false;
        } else {
            fal = true;
        }
    } else if (op.is_show_bz) {
        if (o.is_bz) {
            fal = false;
        } else {
            fal = true;
        }
    }
    return fal;
};

可以看出這段程式碼主要的邏輯就是根據幾個欄位來判斷出最終的結果輸出布林值。最終筆者給出的解答是——

const isRemove = (op, o) => (op.is_show_lock && !o.is_lock) || (op.is_show_sf && !o.is_sf) || (op.is_show_bz && !o.is_bz)

那麼我是如何簡化呢?當看到上面一長串的邏輯判斷的時候,顯而易見的就是存在太多欄位的重複使用,筆者第一感覺就是應該可以用布林代數來簡化,這裡就涉及到兩個知識點了,布林代數的法則以及布林代數運算律,關於這兩個的理論知識就不做贅述,可點選連結檢視或者使用習慣的搜尋引擎進行搜尋查閱相關資料。我們直接進入正題。

簡化

首先,我們再回顧下題主的程式碼可以看出滿足true的條件較少【ps:因為false不僅包含了if中的條件,還包括了不滿足else的情況,這也是筆者第一次回答有誤時的bug之處】,那麼就列出所有滿足true的布林表示式如下:

op.is_show_lock && op.is_show_sf && op.is_show_bz && !(o.is_lock && o.is_sf && o.is_bz) || 
op.is_show_lock && op.is_show_sf && !(o.is_lock && o.is_sf) ||
op.is_show_lock && op.is_show_bz && !(o.is_lock && o.is_bz) ||
op.is_show_sf && op.is_show_bz && !(o.is_sf && o.is_bz) || 
op.is_show_lock && !o.is_lock ||
op.is_show_sf && !o.is_sf ||
op.is_show_bz && !o.is_bz

欄位不過是一種表示,所以這裡可以用字母來代替以便於簡化表示式。假設:

op.is_show_lock -> A
op.is_show_sf -> B
op.is_show_bz -> C
o.is_lock -> a
o.is_show_lock -> b
o.is_show_lock -> c

又因為要表示為數學語言,在數學中:

&& -> ·
|| -> +
! -> ′

於是,程式碼則表示為如下布林代數式:

A · B · C · (a · b · c)′ + 
A · B · (a · b)′ +
A · C · (a · c)′ +
B · C · (b · c)′ + 
A · a′ +
B · b′ +
C · c′

接下來就是對此布林代數式套用布林運算律對其簡化得出最終最優的布林代數。

步驟

  1. 德·摩根律(反演律):(a+b)′=a′·b′,(a·b)′=a′+b′.

    A · B · C · (a′ + b′ + c′) + 
    A · B · (a′ + b′) +
    A · C · (a′ + c′) +
    B · C · (b′ + c′) + 
    A · a′ +
    B · b′ +
    C · c′
  2. 分配律:a·(b+c)=(a·b)+(a·c),(a+b)·c=(a·c)+(b·c)
    從1步驟的結果看出所有abc均為a′b′c′,那麼這裡再做一次替換,各自表示為xyz;然後根據分配律可得:

    A · B · C · x + A · B · C · y + A · B · C · z + 
    A · B · x + A · B · y +
    A · C · x + A · C · z +
    B · C · y + B · C · z + 
    A · x +
    B · y +
    C · z
  3. 交換律:a+b=b+a, a·b=b·a.結合律:(a+b)+c=a+(b+c) ,(a·b)·c=a·(b·c).
    從第2步驟中可以看出存在兩兩子項有公共代數式,如:A · B · x,A · x;根據交換律和結合律,可對其組合運算,這裡暫以這兩個為例:
    A · B · x + A · x
  4. 零一律(么元律):a+0=a, a·1=a.
    A · B · x + A · x · 1
  5. 分配律:a·(b+c)=(a·b)+(a·c),(a+b)·c=(a·c)+(b·c)
    A · x · (B + 1)
  6. 囿元律(極元律):a+1=1, a·0=0.
    A · x
  7. 根據3~6步驟的運算,得出了A · B · x + A · x = A · x ,重複以上步驟對其他兩兩子代數運算最終得出代數式:

    A · x +
    B · y +
    C · z

    至此,布林代數式已不可再優化,那麼就可以將其按照起初約定的轉為如下程式碼:

    (op.is_show_lock && !o.is_lock) || 
    (op.is_show_sf && !o.is_sf) || 
    (op.is_show_bz && !o.is_bz)

寫在最後

在我作為開發的5年時間裡,尚未碰到題主的情況,出現這種程式碼的情況概率應該也比較少,即使碰到,大多數人可能也會用回答中的其他方案,但大多逃離不了迴圈,複雜度直接從O(1)變為O(N),雖然最終的影響微乎其微,但瞭解下其他更優的方案也是百利無害的,從可讀性上來說也有提升,讀者若是遇到此類程式碼可以嘗試用下這個技巧。

相關文章