上回說到,為解荷蘭國旗問題,我們至少有兩種恆式(invariant)可選。一是把未知區段放旁邊:一是把未知區段夾在某兩色之間:
該用哪個比較好呢?
答案是:用兩者都寫得出可運作的程式,但後者好寫得多。原因是:未知區段是我們的工作區,而我們有三種顏色要搬來搬去,把未知區段放在旁邊會使得某些搬移相當不便。
因此以下我們用第二個恆式,寫成邏輯式子是這樣的:
M ≤ r ≤ w ≤ b ≤ N ∧
(∀i : M ≤ i < r : red(i)) ∧
(∀i : r ≤ i < w : white(i)) ∧
(∀i : b ≤ i < N : blue(i))
最初若把 r, w, b
設為 M, M, N
,恆式自然成立 --- M ≤ i < r
, r ≤ i < w
, 和 b ≤ i < N
三個區段都是空的,整個陣列都視為灰色的未知區。接下來我們希望在一個迴圈中逐漸縮小 w
和 b
的差距。當 w = b
時,陣列就照我們想要的顏色順序排好了。
迴圈中應該作什麼呢?既然目標是把 b - w
變小,關鍵的元素可能是 a[w]
和 a[b-1]
. 也許我們可以先看看 a[w]
是紅、白、或藍色,分三種情況處理:或著我們也可檢查 a[b-1]
的顏色。個中優劣只有試過才知道了。依後見之明,我們發現先檢查 a[w]
較好。另一種選擇待會兒再談。
所以,程式架構大約是這樣:
r, w, b, := M, M, N
{ Inv: M ≤ r ≤ w ≤ b ≤ N ∧ Pr ∧ Pw ∧ Pb, bound: b - w }
; do w < b →
if red(w) → Sr
| white(w) → Sw
| blue(w) → Sb
fi
od
{ M ≤ r ≤ w ≤ b ≤ N ∧ Pr ∧ Pw ∧ Pb ∧ b = w }
其中 Pr
, Pw
, 和 Pb
分別是關於三個顏色之條件的簡寫:
Pr ≡ (∀i : M ≤ i < r : red(i))
Pw ≡ (∀i : r ≤ i < w : white(i))
Pb ≡ (∀i : b ≤ i < N : blue(i))
我們還需找出 Sr
, Sw
, 和 Sb
分別該是什麼。
white(w)
的情形最簡單:把 w
遞增之後恆式仍然滿足。Sw
可以是 w := w + 1
.
blue(w)
的情況呢?若把 a[w]
和 a[b]
交換,我們可把 b
遞減,恆式也剛好滿足。Sb
可以是 swap(b,w); b := b - 1
.
最複雜的是 red(w)
的情況。我們可把紅元素換到後面去,但之後為了滿足恆式,我們得把 r
和 w
都遞增。
總結下來,我們的程式是:
r, w, b, := M, M, N
{ Inv: M ≤ r ≤ w ≤ b ≤ N ∧ Pr ∧ Pw ∧ Pb, bound: b - w }
; do w < b →
if red(w) → swap(r,w); r, w := r + 1, w + 1
| white(w) → w := w + 1
| blue(w) → swap(b,w); b := b - 1
fi
od
{ M ≤ r ≤ w ≤ b ≤ N ∧ Pr ∧ Pw ∧ Pb ∧ b = w }
然而,上面的圖片其實遺漏了許多情況。例如,在 red(w)
的情形中,如果 b = w
,程式還會對嗎?我們得切記畫圖僅可幫助理解,程式的推導驗證還是得回到形式系統來作。
以 red(w)
的情況為例,我們得證明:
{ M ≤ r ≤ w < b ≤ N ∧ Pr ∧ Pw ∧ Pb ∧ red(w) }
swap(r,w)
{ (M ≤ r ≤ w ≤ b ≤ N ∧ Pr ∧ Pw ∧ Pb)[r, w := r + 1, w + 1] }
; r, w := r + 1, w + 1
{ M ≤ r ≤ w ≤ b ≤ N ∧ Pr ∧ Pw ∧ Pb }
據我所知,在國內只有 FLOLAC 程式建構課教過這種證明(如果你會的話,來認識一下吧!),所以除非有人要求,這裡就只透露我們得分出 r = w
和 r < w
的兩種情形,不深入談細節了。
這是一個 O(N-M) 的程式。如果三色數量相同,我們平均需要大約 2(N-M)/3 次交換。如果我們當初選擇從檢查 a[b-1]
開始,也可做出一個稍微不同的程式,但平均需要的交換次數增加到 N-M 次。
下圖是排序 [w,r,b,b,r,b,r,r,b,r,r,w,r,r]
的例子。Sr
和 Sb
兩種情況的交換分別用 和 表示。這個例子中,陣列有 14 個元素,而我們用了 13 次交換。