3.10 笛摩根定律

寫條件時常會遇到這種情況:你想判斷的「正面」條件很難直接寫,但它的「反面」(相反情況)卻很好想。 這時有一個好用的工具——笛摩根定律(De Morgan's laws)——可以幫你把「反面」乾淨地翻回「正面」。

兩條定律

先看兩個生活例子。假設「兩科都及格」的意思是「國文及格 && 數學及格」。那什麼情況不算兩科都及格?——只要國文沒過 || 數學沒過,就不算了(任一科沒過就不行)。

反過來,假設「今天有吃到東西」的意思是「中午有吃 || 晚上有吃」(吃到一餐就算)。那什麼情況算整天都沒吃?——中午沒吃 && 晚上沒吃(兩餐都沒吃)。

把上面的「國文及格」「數學及格」換成任意兩個條件(我們用 PQ 當代號——你可以把它想成任何一個「會成立或不成立」的判斷,例如 x > 0score >= 60),就得到笛摩根定律。它講的是「否定(!)碰到 &&|| 時會怎麼變」:

  • !(P && Q) 等於 !P || !Q — 「不是(P 且 Q)」=「非 P 非 Q」
  • !(P || Q) 等於 !P && !Q — 「不是(P 或 Q)」=「非 P 非 Q」

用白話說:

  • 不是(兩個都成立)」=「至少有一個不成立」。
  • 不是(有任一個成立)」=「兩個都不成立」。

重點:否定穿過括號時,裡面每一項都要取反,而且 &&|| 會互換。

用真值表驗證

不用死背,列一張真值表就能確認第一條定律成立(T=true、F=false):

P Q !(P && Q) !P || !Q
T T F F
T F T T
F T T T
F F T T

兩欄結果一模一樣,!(P && Q)!P || !Q 確實相等。(第二條定律你可以自己照樣列一張表驗證。)

例題:兩區間是否重疊

來看一道乍看「條件很不直覺」的題目——區間重疊判斷(rangeintc)

數線上有兩個閉區間 [a_1, b_1][a_2, b_2],問它們有沒有共同的點(是否重疊)。

先看懂題目用詞:閉區間 [a, b] 指的是數線上「從 ab包含兩個端點 ab」的整段範圍——中括號 [ ] 就是「含端點」的記號。區間題幾乎都會用這個詞,務必分清楚含不含端點(呼應 3.2 說的邊界問題)。

「重疊」要直接寫成條件並不好想。但「不重疊」超好想:兩個區間完全錯開時才不重疊,也就是其中一個整個落在另一個的左邊:

  • 區間 1 整個在區間 2 左邊:b1 < a2(區間 1 的右端比區間 2 的左端還小)
  • 區間 2 整個在區間 1 左邊:b2 < a1

所以:

不重疊  =  (b1 < a2) || (b2 < a1)

而「重疊」就是「不重疊」的相反,加個 ! 即可。這時笛摩根定律登場,把 ! 翻進括號:

重疊 = !( (b1 < a2) || (b2 < a1) )
     = !(b1 < a2) && !(b2 < a1)     ← 笛摩根:! 穿過,|| 變 &&
     = (b1 >= a2) && (b2 >= a1)     ← 把每項的 < 取反成 >=

於是就得到那個「看起來很不直覺、卻絕對正確」的寫法:

int a1, b1, a2, b2;
cin >> a1 >> b1 >> a2 >> b2;

// 重疊 = 不是「兩區間完全錯開」,用笛摩根化簡後就是這一行
if (b1 >= a2 && b2 >= a1) {
    cout << "Yes\n";
} else {
    cout << "No\n";
}

以後遇到「正面難寫、反面好想」的條件,就先寫反面、再用笛摩根定律翻成正面——又快又不容易錯。

動手試試看

先在紙上用笛摩根定律驗證:!(x >= 0 && x <= 100) 展開後等於 x < 0 || x > 100(想想看它的意思是「不在 0 到 100 之間」)。接著把 區間重疊判斷(rangeintc) 寫出來並通過。