
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>小眾計算學 &#187; Hoare 邏輯</title>
	<atom:link href="http://www.iis.sinica.edu.tw/~scm/ncs/tag/hoare-logic/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.iis.sinica.edu.tw/~scm/ncs</link>
	<description>for the few of us.</description>
	<lastBuildDate>Fri, 09 Dec 2011 23:27:40 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	
		<item>
		<title>荷蘭國旗問題 The Dutch National Flag Problem（下）</title>
		<link>http://www.iis.sinica.edu.tw/~scm/ncs/2010/10/dutch-national-flag-problem-3/</link>
		<comments>http://www.iis.sinica.edu.tw/~scm/ncs/2010/10/dutch-national-flag-problem-3/#comments</comments>
		<pubDate>Mon, 18 Oct 2010 04:45:25 +0000</pubDate>
		<dc:creator>Shin</dc:creator>
				<category><![CDATA[計算算計]]></category>
		<category><![CDATA[Hoare 邏輯]]></category>
		<category><![CDATA[荷蘭國旗問題]]></category>

		<guid isPermaLink="false">http://www.iis.sinica.edu.tw/~scm/ncs/?p=226</guid>
		<description><![CDATA[為什麼這麼慢呢？Bentley 和 McIlroy 認為問題出在我們花了太多工夫把兩個白色元素一步步挪到中間。與其如此，不如先把白色元素放到兩邊去，把陣列排成「白、紅、藍、白」。等排序完成，再把白色換回中間就可以了！]]></description>
			<content:encoded><![CDATA[<p>Quicksort 是實用上表現很好的排序法。眾所皆知，quicksort 的缺點是碰到大致上已排好的、大致上逆向排好的、或著有許多重複元素的陣列時，效率就變差了。若把荷蘭國旗問題用在 quicksort 中，用紅、白、藍三色分別代表小於、等於、大於 pivot 的元素，由於等於 pivot 的元素已被放在一起，應有易於處理重複元素的好處。然而，直到 90 年代中期之前，大家的一般認知是解荷蘭國旗問題需要太多次交換，不值得做。一般做法是把大陣列先用 quicksort 分割，等切得夠小就換成 bucket sort 或 radix sort. </p>
<p>到了 90 年代， Lee McMahon 為第七版 UNIX 寫的 quicksort 函式和其衍生版本已經流傳將近二十年之久。Bentley 和 McIlroy 發現，對所有他們能找到的 quicksort 實作，都可很容易地做個輸入讓它得花輸入長度平方的時間跑完。於是他們決定寫個更有效的 quicksort. 為了處理重複元素，他們又回頭研究了荷蘭國旗問題。</p>
<p><a href="http://www.iis.sinica.edu.tw/~scm/ncs/2010/10/dutch-national-flag-problem-2/">上次</a>我們看了排序 <code>[w,r,b,b,r,b,r,r,b,r,r,w,r,r]</code> 的例子。<code>Sr</code> 和 <code>Sb</code> 兩種情況的交換分別用 <img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-swap-Sr.jpg" style="margin:0"/> 和 <img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-swap-Sb.jpg" style="margin:0"/> 表示。這個例子中，陣列有 14 個元素，而我們用了 13 次交換。</p>
<p><img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-trace.jpg" class="aligncenter"/></p>
<p>為什麼這麼慢呢？Bentley 和 McIlroy 認為問題出在我們花了太多工夫把兩個白色元素一步步挪到中間（也就是發生<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-swap-Sr.jpg" style="margin:0"/>的情況）。與其如此，不如先把白色元素放到兩邊去，把陣列排成「白、紅、藍、白」。等排序完成，再把白色換回中間就可以了！他們用的不變量是這樣：<br />
<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-bm-inv.jpg" class="aligncenter"/></p>
<pre><code>M ≤ w ≤ r ≤ u ≤ b ≤ N ∧ Pw1 ∧ Pr ∧ Pb ∧ Pw2

Pw1 ≡(∀i : M ≤ i < w : white(i))
Pr ≡ (∀i : w ≤ i < r : red(i))
Pb ≡ (∀i : u ≤ i < b : blue(i))
Pw2 ≡ (∀i : b ≤ i < N : white(i))</code></pre>
<p>我用這不變量寫了一個程式：</p>
<pre><code>  w, r, u, b := M, M, N, N
  <span class="comment">{ Inv: M ≤ w ≤ r ≤ u ≤ b ≤ N ∧ Pw1 ∧ Pr ∧ Pb ∧ Pw2, bound: b - w }</span>
; do r < u →
    if red(r)             → r := r + 1
     | white(r)           → swap(w,r); w, r := w + 1, r + 1
     | blue(r) ∧ red(u-1) → swap(r,u-1); r, u := r + 1, u - 1
     | white(u-1)         → swap(u-1,b-1);  u, b := u - 1, b - 1
     | blue(u-1)          → u := u - 1
    fi
  od
  <span class="comment">{ M ≤ w ≤ r ≤ u ≤ b ≤ N ∧ Pw1 ∧ Pr ∧ Pb ∧ Pw2 ∧ r = u }</span></code></pre>
<p>迴圈主體是對稱的五個情況，其中三種需要交換。以下是執行同一個輸入的結果：<br />
<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-bm-trace.jpg" class="aligncenter"/><br />
確實不僅迴圈執行次數變少（由於 <code>blue(r) ∧ red(u-1)</code> 的情況可把灰色區域縮小兩個元素），交換次數也減少到了六次。當然，我們還需要兩次交換把白色元素放回中間。如果白色元素多，交換次數就多。但 Bentley 和 McIlroy 指出這部份的代價可分配到演算法其他部份去，因為白色元素一旦放到正確位置，便不用再被遞迴排序了。更詳盡的實驗結果可看他們的論文。</p>
<p>這兒的程式和 Bentley-McIlroy 的稍有不同。Bentley-McIlroy 用了兩個迴圈分別推進 r 與 u 的值。我個人比較喜歡這裡的做法，一來迴圈主體和不變量一樣對稱，二來這個程式非必須的限制較少：例如當 red(r) 與 blue(u-1) 都成立，在這裡可看出執行哪個指令對程式的正確性沒有影響。</p>
<p><img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/POL_Rypin_flag.png" class="alignright"/><br />
只是，這麼一來，這迴圈解的好像不是荷蘭國旗問題了。有什麼國旗是「白、紅、藍、白」四色的呢？我找不出分出四條色塊的國旗。如果把顏色交換一下，倒是找到了右邊這幅 <a href="http://en.wikipedia.org/wiki/Rypin">Rypin</a> 的旗幟。Rypin 是波蘭的一個小鎮，根據 06 年的統計，人口約一萬六千人。</p>
<h3>References</h3>
<p>Jon L. Bentley, M. Douglas McIlroy. <a href="http://portal.acm.org/citation.cfm?id=172710">Engineering a sort function</a>. Software—Practice &#038; Experience 23(11), pp. 1249 - 1265. Nov. 1993.</p>
<p>Justin Peel. <a href="http://stackoverflow.com/questions/2105737/has-anyone-seen-this-improvement-to-quicksort-before">Has anyone seen this improvement to quicksort before?</a> Stack Overflow , Jan 20, 2010.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.iis.sinica.edu.tw/~scm/ncs/2010/10/dutch-national-flag-problem-3/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>荷蘭國旗問題 The Dutch National Flag Problem（中）</title>
		<link>http://www.iis.sinica.edu.tw/~scm/ncs/2010/10/dutch-national-flag-problem-2/</link>
		<comments>http://www.iis.sinica.edu.tw/~scm/ncs/2010/10/dutch-national-flag-problem-2/#comments</comments>
		<pubDate>Sat, 16 Oct 2010 14:28:07 +0000</pubDate>
		<dc:creator>Shin</dc:creator>
				<category><![CDATA[計算算計]]></category>
		<category><![CDATA[Hoare 邏輯]]></category>
		<category><![CDATA[荷蘭國旗問題]]></category>

		<guid isPermaLink="false">http://www.iis.sinica.edu.tw/~scm/ncs/?p=187</guid>
		<description><![CDATA[這是一個 O(N-M) 的程式。如果三色數量相同，我們平均需要大約 2(N-M)/3 次交換。如果我們當初選擇從檢查 <code>a[b-1]</code> 開始，平均需要的交換次數則是 N-M 次。]]></description>
			<content:encoded><![CDATA[<p><a href="http://www.iis.sinica.edu.tw/~scm/ncs/2010/10/dutch-national-flag-problem/">上回</a>說到，為解荷蘭國旗問題，我們至少有兩種不變量可選。一是把未知區段放旁邊：<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-inv-1-label.jpg" class="aligncenter"/>一是把未知區段夾在某兩色之間：<br />
<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-inv-2-label.jpg" class="aligncenter"/>該用哪個比較好呢？</p>
<p>答案是：用兩者都寫得出可運作的程式，但後者好寫得多。原因是：未知區段是我們的工作區，而我們有三種顏色要搬來搬去，把未知區段放在旁邊會使得某些搬移相當不便。</p>
<p>因此以下我們用第二個不變量，寫成邏輯式子是這樣的：</p>
<pre><code>M ≤ r ≤ w ≤ b ≤ N ∧
(∀i : M ≤ i < r : red(i)) ∧
(∀i : r ≤ i < w : white(i)) ∧
(∀i : b ≤ i < N : blue(i))</code></pre>
<p>最初若把 <code>r, w, b</code> 設為 <code>M, M, N</code>，不變量自然成立 --- <code>M ≤ i < r</code>, <code>r ≤ i < w</code>, 和 <code>b ≤ i < N</code> 三個區段都是空的，整個陣列都視為灰色的未知區。接下來我們希望在一個迴圈中逐漸縮小 <code>w</code> 和 <code>b</code> 的差距。當 <code>w = b</code> 時，陣列就照我們想要的顏色順序排好了。</p>
<p>迴圈中應該作什麼呢？既然目標是把 <code>b - w</code> 變小，關鍵的元素可能是 <code>a[w]</code> 和 <code>a[b-1]</code>. 也許我們可以先看看 <code>a[w]</code> 是紅、白、或藍色，分三種情況處理：或著我們也可檢查 <code>a[b-1]</code> 的顏色。個中優劣只有試過才知道了。依後見之明，我們發現先檢查 <code>a[w]</code> 較好。另一種選擇待會兒再談。</p>
<p>所以，程式架構大約是這樣：
<pre><code>   r, w, b, := M, M, N
   <span class="comment">{ Inv: M ≤ r ≤ w ≤ b ≤ N ∧ Pr ∧ Pw ∧ Pb, bound: b - w }</span>
 ; do w < b →
     if red(w)   → <span class="comment">Sr</span>
      | white(w) → <span class="comment">Sw</span>
      | blue(w)  → <span class="comment">Sb</span>
     fi
   od
   <span class="comment">{ M ≤ r ≤ w ≤ b ≤ N ∧ Pr ∧ Pw ∧ Pb ∧ b = w }</span></code></pre>
<p>其中 <code>Pr</code>, <code>Pw</code>, 和 <code>Pb</code> 分別是關於三個顏色之條件的簡寫：</p>
<pre><code>Pr ≡ (∀i : M ≤ i < r : red(i))
Pw ≡ (∀i : r ≤ i < w : white(i))
Pb ≡ (∀i : b ≤ i < N : blue(i))</code></pre>
<p>我們還需找出 <code>Sr</code>, <code>Sw</code>, 和 <code>Sb</code> 分別該是什麼。</p>
<p><code>white(w)</code> 的情形最簡單：把 <code>w</code> 遞增之後不變量仍然滿足。<code>Sw</code> 可以是 <code>w := w + 1</code>.<br />
<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-inv-Sw.jpg" class="aligncenter"/><br />
<code>blue(w)</code> 的情況呢？若把 <code>a[w]</code> 和 <code>a[b]</code> 交換，我們可把 <code>b</code> 遞減，不變量也剛好滿足。<code>Sb</code> 可以是 <code>swap(b,w); b := b - 1</code>.<br />
<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-inv-Sb.jpg" class="aligncenter"/><br />
最複雜的是 <code>red(w)</code> 的情況。我們可把紅元素換到後面去，但之後為了滿足不變量，我們得把 <code>r</code> 和 <code>w</code> 都遞增。<br />
<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-inv-Sr.jpg" class="aligncenter"/><br />
總結下來，我們的程式是：
<pre><code>  r, w, b, := M, M, N
  <span class="comment">{ Inv: M ≤ r ≤ w ≤ b ≤ N ∧ Pr ∧ Pw ∧ Pb, bound: b - w }</span>
; 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
  <span class="comment">{ M ≤ r ≤ w ≤ b ≤ N ∧ Pr ∧ Pw ∧ Pb ∧ b = w }</span></code></pre>
<p>然而，上面的圖片其實遺漏了許多情況。例如，在 <code>red(w)</code> 的情形中，如果 <code>b = w</code> ，程式還會對嗎？我們得切記畫圖僅可幫助理解，程式的推導驗證還是得回到形式系統來作。</p>
<p>以 <code>red(w)</code> 的情況為例，我們得證明：</p>
<pre><code>  <span class="comment">{ M ≤ r ≤ w < b ≤ N ∧ Pr ∧ Pw ∧ Pb ∧ red(w) }</span>
  swap(r,w)
  <span class="comment">{ (M ≤ r ≤ w ≤ b ≤ N ∧ Pr ∧ Pw ∧ Pb)[r, w := r + 1, w + 1] }</span>
; r, w := r + 1, w + 1
  <span class="comment">{ M ≤ r ≤ w ≤ b ≤ N ∧ Pr ∧ Pw ∧ Pb }</span>
</code></pre>
<p>據我所知，在國內只有 <a href="http://flolac.iis.sinica.edu.tw/flolac10/">FLOLAC</a> 程式建構課教過這種證明（如果你會的話，來認識一下吧！），所以除非有人要求，這裡就只透露我們得分出 <code>r = w</code> 和 <code>r < w </code> 的兩種情形，不深入談細節了。</p>
<p>這是一個 O(N-M) 的程式。如果三色數量相同，我們平均需要大約 2(N-M)/3 次交換。如果我們當初選擇從檢查 <code>a[b-1]</code> 開始，也可做出一個稍微不同的程式，但平均需要的交換次數增加到 N-M 次。</p>
<p>下圖是排序 <code>[w,r,b,b,r,b,r,r,b,r,r,w,r,r]</code> 的例子。<code>Sr</code> 和 <code>Sb</code> 兩種情況的交換分別用 <img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-swap-Sr.jpg" style="margin:0"/> 和 <img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-swap-Sb.jpg" style="margin:0"/> 表示。這個例子中，陣列有 14 個元素，而我們用了 13 次交換。</p>
<p><img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-trace.jpg" class="aligncenter"/></p>
]]></content:encoded>
			<wfw:commentRss>http://www.iis.sinica.edu.tw/~scm/ncs/2010/10/dutch-national-flag-problem-2/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>荷蘭國旗問題 The Dutch National Flag Problem（上）</title>
		<link>http://www.iis.sinica.edu.tw/~scm/ncs/2010/10/dutch-national-flag-problem/</link>
		<comments>http://www.iis.sinica.edu.tw/~scm/ncs/2010/10/dutch-national-flag-problem/#comments</comments>
		<pubDate>Fri, 15 Oct 2010 02:06:59 +0000</pubDate>
		<dc:creator>Shin</dc:creator>
				<category><![CDATA[計算算計]]></category>
		<category><![CDATA[Edsger Dijkstra]]></category>
		<category><![CDATA[Hoare 邏輯]]></category>
		<category><![CDATA[荷蘭國旗問題]]></category>

		<guid isPermaLink="false">http://www.iis.sinica.edu.tw/~scm/ncs/?p=154</guid>
		<description><![CDATA[陣列裡頭每個元素都是紅、白、藍三色之一。如何把它們由左至右依紅、白、藍的順序排好呢？Dijkstra 希望採用三向分割法後能更容易表達紅（小於 pivot）和藍色（大於 pivot）的區塊絕對比原陣列短的性質。然而，在 Peter 的印象中 Dijkstra 從沒把這層考量寫下來。「我們如果不告訴學生，以後就沒人知道了呢！」他說。]]></description>
			<content:encoded><![CDATA[<p><img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-prob.jpg" class="alignright"/><br />
一個陣列 <code>a[M], a[M+1] ... a[N-1]</code> 裡頭每個元素都是紅、白、藍三色之一。如何把它們由左至右依紅、白、藍的順序排好呢？您可用 <code>red</code>, <code>white</code>, 和 <code>blue</code> 三個述語測元素的顏色；要改變陣列內容則只能用 <code>swap(i,j)</code>，交換索引為 i 和 j 的兩個元素（因此不能先把紅白藍三色的數目分別數出來之後再把陣列整個重寫一次，也不能把 a 的內容複製到別處之後再複製回來）。整件工作希望在 O(N-M) 的時間內完成。</p>
<p><img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/flag.gif" class="alignright"/></p>
<p>這個「荷蘭國旗問題」由 Feijen 和 Dijkstra 提出 &#8212; 紅白藍是荷蘭國旗的顏色。（為何不是法國國旗？恐怕因為 Feijen 和 Dijkstra 都是荷蘭人吧？）對演算法熟悉的朋友可能知道這可在 quicksort 中作分割用：選定一個元素當 pivot, 紅、白、藍三色分別代表小於、等於、和大於 pivot 的元素。和傳統將陣列分割成兩塊的做法比較，這種「三向」分割法不僅可能把陣列切得較小，也便於處理陣列中的重複元素。然而，Sedgewick 和 Wayne 的上課資料 (<a href="http://www.cs.princeton.edu/courses/archive/spr07/cos226/lectures/sort2.pdf">1</a>, <a href="http://www.cs.princeton.edu/courses/archive/spr10/cos226/lectures/06-23Quicksort-2x2.pdf">2</a>)提到，直到 90 年代中，大家仍認為三向分割法效率差，不值得做。直到 Bentley 與 McIlroy 提出了<a href="http://portal.acm.org/citation.cfm?id=172710">更快的三向分割法</a>，Sedgewick 和 Bentley 也證明了<a href="http://www.sorting-algorithms.com/static/QuicksortIsOptimal.pdf">此種 quicksort 是熵值最佳解</a> &#8212; 每元素平均被比較數在熵值的常數比率之內，三向分割法終於漸漸被用在包括 Java 與 C 的函式庫中。</p>
<p>九月的 IFIP Working Group 2.1 會議中，Wouter Swierstra 示範<a href="http://www.cse.chalmers.se/~wouter/Talks/FPDag10.pdf">如何在 Agda 中證明此程序能正常終止</a>，引起了一些相關討論。我才因此聽 Peter Pepper 說到，Dijkstra 當初研究這問題倒不是基於效率考量，而是他認為分割成兩份的傳統 quicksort 的終止證明不漂亮。Dijkstra 希望採用三向分割法後能更容易表達紅（小於 pivot）和藍色（大於 pivot）的區塊絕對比原陣列短的性質。然而，在 Peter 的印象中 Dijkstra 從沒把這層考量寫下來。「我們如果不告訴學生，以後就沒人知道了呢！」他說。</p>
<p>解一個問題前總得先看看如何精確描述我們要什麼。初始條件是 <code>M ≤ N</code>, 以及每個元素的顏色只可能是紅白藍之一：</p>
<pre><code>M ≤ N ∧ (∀i : M ≤ i < N : red(i) ∨ white (i) ∨ blue(i))</code></pre>
<p>而在程式結束時，我們希望找到 <code>r</code> 和 <code>w</code> 兩值, 滿足：</p>
<pre><code>M ≤ r ≤ w ≤ N ∧
(∀i : M ≤ i < r : red(i)) ∧                                  (*)
(∀i : r ≤ i < w : white(i)) ∧
(∀i : w ≤ i < N : blue(i))</code></pre>
<p>注意在此我們用 Dijkstra 常使用的方法，以一對變數 <code>x</code>, <code>y</code> 表示 <code>a[x], a[x+1] .. a[y-1]</code> 這個區段（較正式的寫法是 <code>a[x..y)</code> -- 左合右開）。一個區段 <code>a[x..y)</code> 的元素數目剛好是 <code>y-x</code>, 而當 <code>x = y</code> 時該區段是空的。前面的規格涵括了輸入為空陣列的可能性（M = N），也允許某些顏色沒有元素（如 <code>r = w</code> 時，輸出陣列沒有白色的元素）。</p>
<p>這種程式至少得用個迴圈，而既然有迴圈，我們得為它找個不變量。通常的作法是把 (*) 擴充。我們也可猜到程式執行中必定有一段「未知」的區段。一個可能性是允許這個未知區段出現在右邊：<br />
<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-inv-1.jpg" class="aligncenter"/><br />
或著，我們也可能允許這段未知區段出現在白色與藍色之間：<br />
<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-inv-2.jpg" class="aligncenter"/><br />
剩下兩種情形則分別與前兩種對稱，因此我們只需考慮前兩者即可。<br />
<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-inv-3-4.jpg" class="aligncenter"/></p>
<p>圖片可以幫助理解，但真正的理解仍得靠形式化的描述。第一種情形可描述為:</p>
<pre><code>M ≤ r ≤ w ≤ b ≤ N ∧
(∀i : M ≤ i < r : red(i)) ∧
(∀i : r ≤ i < w : white(i)) ∧
(∀i : w ≤ i < b : blue(i))</code></pre>
<p>那麼我們可採用的策略就是最初讓 <code>r,w,b := M,M,M</code>，每次在迴圈中試著遞增 <code>b</code>, 當 <code>b = N</code>, <code>(*)</code> 就成立了。<br />
<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-inv-1-label.jpg" class="aligncenter"/></p>
<p>若選擇第二種可能，形式化的描述是:</p>
<pre><code>M ≤ r ≤ w ≤ b ≤ N ∧
(∀i : M ≤ i < r : red(i)) ∧
(∀i : r ≤ i < w : white(i)) ∧
(∀i : b ≤ i < N : blue(i))</code></pre>
<p>三個變數可先設為 <code>r,w,b := M,M,N</code>, 如果 <code>b = w</code>, <code>(*)</code> 便能成立。<br />
<img src="http://www.iis.sinica.edu.tw/~scm/img/2010/dutch/dutch-inv-2-label.jpg" class="aligncenter"/></p>
<p>哪個不變量比較好用呢？讀者不妨試試看。我們下回分解。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.iis.sinica.edu.tw/~scm/ncs/2010/10/dutch-national-flag-problem/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>距離平方和</title>
		<link>http://www.iis.sinica.edu.tw/~scm/ncs/2010/07/sum-of-square-of-distance/</link>
		<comments>http://www.iis.sinica.edu.tw/~scm/ncs/2010/07/sum-of-square-of-distance/#comments</comments>
		<pubDate>Sat, 10 Jul 2010 10:14:10 +0000</pubDate>
		<dc:creator>Shin</dc:creator>
				<category><![CDATA[計算算計]]></category>
		<category><![CDATA[FLOLAC]]></category>
		<category><![CDATA[Hoare 邏輯]]></category>
		<category><![CDATA[程式推導]]></category>

		<guid isPermaLink="false">http://www.iis.sinica.edu.tw/~scm/ncs/?p=68</guid>
		<description><![CDATA[給定一個有兩個以上元素的陣列<code>a</code>, 計算任兩個元素前者減後者所得之差的平方的總和。]]></description>
			<content:encoded><![CDATA[<p><a href="http://flolac.iis.sinica.edu.tw/flolac10/">FLOLAC &#8217;10</a> <a href="http://flolac.iis.sinica.edu.tw/flolac10/doku.php?id=prog">程式推導</a>課的期末考出了這題：<br />
<code>
<pre>|[ con N <span class="comment">{N ≥ 2}</span>; a : array [0..N) of int;
   var r : int;
   S
   <span class="comment">{ r = (Σ i,j : 0 ≤ i < j < N : (a.i - a.j)²) }</span>
]|</pre>
<p></code>用口語說：給定一個有兩個以上元素的陣列<code>a</code>, 計算任兩個元素前者減後者所得之差的平方的總和（這裡依照 guarded command language 的傳統把 <code>a</code> 的第 <code>i</code> 個元素寫成 <code>a.i</code>；函數取值 <code>f(x)</code> 也寫成 <code>f.x</code>）。</p>
<p>大家大概都能寫出一個使用兩層迴圈的程式。蠻可惜地，發現這題其實能只用一個迴圈、在線性時間內做完的則不多，因此我想可在這討論一下。</p>
<h3>量詞 (Quantifiers)</h3>
<p>先回顧一下關於量詞 (quantifiers) 的規則。給定一個滿足交換律、結合律、有單位元素 <code>e</code> 的二元運算元 <code>⊕</code>。若將所有在區段 <code>[A .. B)</code> 之間的值非正式地寫成 <code>i₀, i₁, i₂ ... i<sub>n</sub></code>，那麼 <code>F.i₀ ⊕ F.i₁ ⊕  F.i₂ ⊕ ... ⊕ F.i<sub>n</sub></code> 就表示成：<br />
<code>
<pre>   (⊕ i : A ≤ i < B : F.i)</pre>
<p></code>更一般地說，若所有滿足 <code>R</code> 的值是 <code>i₀, i₁, i₂ ... i<sub>n</sub></code>，這個式子<br />
<code>
<pre>   (⊕ i : R.i : F.i)</pre>
<p></code>表示的就是<code>F.i₀ ⊕ F.i₁ ⊕  F.i₂ ⊕ ... ⊕ F.i<sub>n</sub></code>. 當確定不會混淆時，我們可把 <code>R.i</code> 與 <code>F.i</code> 中的 <code>i</code> 省略。 </p>
<p>關於量詞的規則有</p>
<ol>
<li><code>(⊕ i : false : F.i) = e</code></li>
<li><code>(⊕ i : i = x : F.i) = F.x</code></li>
<li><code>(⊕ i : R : F) ⊕ (⊕ i : S : F) = (⊕ i : R ∨ S : F) ⊕ (⊕ i : R ∧ S : F)</code></li>
<li><code>(⊕ i : R : F) ⊕ (⊕ i : R : G) = (⊕ : R : F ⊕ G)</code></li>
<li><code>(⊕ i : R.i : (⊕ j : S.j : F.i.j)) = (⊕ j : S.j : (⊕ i : R.i : F.i.j))</code></li>
</ol>
<p>常用的「分出第<code>n</code>項」是規則 1 和 3 的結果：若已知 <code>n > 0</code>,  我們可把區段 <code>0 ≤ i < n + 1</code> 分為無交集的兩塊 -- <code>0 ≤ i < n</code> 和 <code>i = n</code>。由規則 3 （左右反轉後）和 1 我們得到：<br />
<code>
<pre>  (⊕ i : 0 ≤ i < n + 1 : F.i) = (⊕ i : 0 ≤ i < n : F.i) ⊕ F.n</pre>
<p></code></p>
<p>兩個變數的量詞可用一個變數的量詞定義:<br />
<code>
<pre>   (⊕ i,j : R.i ∧ S.i,j : F.i.j) = (⊕ i : R.i : (⊕ j : S.i.j : F.i.j))</pre>
<p></code>如果 <code>⊗</code> 可分配到 <code>⊕</code> 中，我們另有這項性質：<br />
<code>
<pre>   x ⊗ (⊕ i : R : F) = (⊗ i : R : x ⊗ F)</pre>
<p></code></p>
<p>習慣上我們常把 <code>(+ i : R : F)</code> 寫成 <code>(Σ i : R : F)</code>.</p>
<h3>計算平方和</h3>
<p>大家都可猜到第一步是把常數 <code>N</code> 換成變數 <code>n</code>。整個程式會是一個迴圈，我們在不變量中試著維持這樣的關係：<br />
<code>
<pre>   P  ≣  r = (Σ i,j : 0 ≤ i < j < n : (a.i - a.j)²)</pre>
<p></code>接下來，我們猜想程式中大概會用這樣一個迴圈，每次把 <code>n</code> 遞增，直到 <code>n</code> 與 <code>N</code> 相碰為止：<br />
<code>
<pre>   <span class="comment">{ Inv: P ∧ 2 ≤ n ≤ N , Bound: N - n}</span>
   do n ≠ N → ... ; n := n + 1 od</pre>
<p></code>那麼現在要討論的就是如何在 <code>n := n + 1</code> 之前更新 <code>r</code> 的值，使得不變量 <code>P</code> 仍能滿足了。</p>
<p>假設 <code>P</code> 與 <code>2 ≤ n ≤ N</code> 成立，我們把 <code>r</code> 的值之中的 <code>n</code> 代換成 <code>n + 1</code> 試試看：<br />
<code>
<pre>   (Σ i,j : 0 ≤ i < j < n : (a.i - a.j)²)[n+1 / n]
 = (Σ i,j : 0 ≤ i < j < n + 1 : (a.i - a.j)²)
 =   <span class="comment">{ split off j = n }</span>
   (Σ i,j : 0 ≤ i < j < n : (a.i - a.j)²) +
   (Σ i : 0 ≤ i < n : (a.i - a.n)²)
 =   <span class="comment">{ P }</span>
   r + (Σ i : 0 ≤ i < n : (a.i - a.n)²)
</pre>
<p></code><br />
大部分人推導至此，會想用一個內層迴圈算 <code>(Σ i,j : 0 ≤ i < n : (a.i - a.n)²)</code>. 但細看這樣寫出的程式，會發現許多計算是重複的。確實，這個式子還可以再拆：<br />
<code>
<pre>   r + (Σ i : 0 ≤ i < n : (a.i - a.n)²)
 =   <span class="comment">{ (x - y)² = x² - 2xy + y² }</span>
   r + (Σ i : 0 ≤ i < n : a.i² - 2 × a.i × a.n + a.n²)
 =   <span class="comment">{ 規則 4 }</span>
   r + (Σ i : 0 ≤ i < n : a.i²)
     - (Σ i : 0 ≤ i < n : 2 × a.i × a.n)
     + (Σ i : 0 ≤ i < n : a.n²)
 =   <span class="comment">{ a.n 已是常數，乘法與加法之分配律 }</span>
   r + (Σ i : 0 ≤ i < n : a.i²)
     - 2 × (Σ i : 0 ≤ i < n : a.i) × a.n
     + (Σ i : 0 ≤ i < n : a.n²)
 =   <span class="comment">{ 化簡最後一項 }</span>
   r + (Σ i : 0 ≤ i < n : a.i²)
     - 2 × (Σ i : 0 ≤ i < n : a.i) × a.n + n × a.n²
</pre>
<p></code>所以我們可另用兩個變數分別儲存 <code>(Σ i : 0 ≤ i < n : a.i²)</code> 和 <code>(Σ i : 0 ≤ i < n : a.i)</code> 的值：<br />
<code>
<pre>  Q₀  ≣  s = (Σ i : 0 ≤ i < n : a.i²)
  Q₁  ≣  t = (Σ i : 0 ≤ i < n : a.i)
</pre>
<p></code><code>s</code> 和 <code>t</code> 兩個變數的更新方式則不難推導出來。最後得到的程式如下：<br />
<code>
<pre>|[ con N <span class="comment">{N ≥ 2}</span>; a : array [0..N) of int;
   var r, s, t, n : int;

   r, s, t, n := (a.0 - a.1)², a.0² + a.1², a.0 + a.1, 2
   <span class="comment">{ Inv: P ∧ Q₀ ∧ Q₁ ∧ 2 ≤ n ≤ N , Bound: N - n }</span>
 ; do n ≠ N →
      r := r + s - 2 × t × a.n + n × a.n²;
      s := s + a.n²;
      t := t + a.n;
      n := n + 1
   od
   <span class="comment">{ r = (Σ i,j : 0 ≤ i < j < N : (a.i - a.j)²) }</span>
]|</pre>
<p></code></p>
<h3>其他解答</h3>
<p>大部分（有寫出答案的）同學寫的是兩層迴圈，<code>O(N²)</code> 的程式。少部份導出了上述的 <code>O(N)</code> 程式（很棒唷！）。另有一位同學給了個我沒想到的解：<br />
<code>
<pre>|[ con N <span class="comment">{N ≥ 2}</span>; a : array [0..N) of int;
   var r, i, j : int;

   r, i, j := 0, 0, 0
   <span class="comment">{ Inv: ... ∧ 0 ≤ i ≤ j ∧ 0 ≤ j ≤ N, Bound: ? }</span>
 ; do j ≠ N →
       if i < j → r := r + (a.i - a.j)²;  i := i + 1
        | i = j → i, j := 0, j + 1
       fi
   od
]|</pre>
<p></code>這仍是一個 <code>O(N²)</code> 的程式，基本上是把兩層 loop 的內層用手動的方式做。但我蠻想看看它的證明。可惜那位同學的程式、給的不變量、和 bound 都有些小瑕疵（可能時間真的不夠吧）。大家想試著證明看看嗎？</p>
]]></content:encoded>
			<wfw:commentRss>http://www.iis.sinica.edu.tw/~scm/ncs/2010/07/sum-of-square-of-distance/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>再看二元搜尋法 Binary Search（下）</title>
		<link>http://www.iis.sinica.edu.tw/~scm/ncs/2010/03/binary-search-revisited-02/</link>
		<comments>http://www.iis.sinica.edu.tw/~scm/ncs/2010/03/binary-search-revisited-02/#comments</comments>
		<pubDate>Sat, 06 Mar 2010 04:43:03 +0000</pubDate>
		<dc:creator>Shin</dc:creator>
				<category><![CDATA[計算算計]]></category>
		<category><![CDATA[Hoare 邏輯]]></category>
		<category><![CDATA[二元搜尋法]]></category>

		<guid isPermaLink="false">http://www.iis.sinica.edu.tw/~scm/ncs/?p=41</guid>
		<description><![CDATA[給定一個排序好的陣列 <code>a[0..N)</code>（其元素為 <code>a[0]</code>, <code>a[1]</code> ... <code>a[N-1]</code>）, <code>0 ≤ N</code>。如何用 van Gasteren 與 Feijen 的方法判斷其中是否含有某個關鍵值 <code>K</code> 呢？]]></description>
			<content:encoded><![CDATA[<p>已知兩整數 <code>M < N</code> 使得 <code>Φ(M,N)</code> 成立。<a href="http://www.iis.sinica.edu.tw/~scm/ncs/2010/03/binary-search-revisited/">上次</a>我們介紹了 <a href="http://www.mathmeth.com/wf/files/wf2xx/wf214.pdf">Netty van Gasteren 和 Wim Feijen</a> 的二元搜尋法，能找到某個 <code>l</code>, <code>M ≤ l < N</code>，使得 <code>Φ(l,l+1)</code> 成立。給定一個排序好的陣列 <code>a[0..N)</code>（其元素為 <code>a[0]</code>, <code>a[1]</code> ... <code>a[N-1]</code>）, <code>0 ≤ N</code>。如何用 van Gasteren 與 Feijen 的方法判斷其中是否含有某個關鍵值 <code>K</code> 呢？</p>
<p>第一個念頭是用這個 <code>Φ</code>:</p>
<pre><code>Φ(i,j) = a[i] ≤ K < a[j]</code></pre>
<p>並令 <code>M = 0</code>。演算法結束後，<code>Φ(l,l+1) = a[l] ≤ K < a[l+1]</code> 一定成立，只要再看看 <code>a[l]</code> 是否等於 <code>K</code> 就可以了。但問題來了：van Gasteren-Feijen 演算法正確的兩個先決條件之一是 <code>M < N</code> -- van Gasteren-Feijen 演算法的迴圈設計便假設 <code>[M,N)</code> 這個區段不是空的，因此我們無法處理空陣列。其次是 <code>Φ(0,N)</code> 得要成立，但 <code>a[0] ≤ K < a[N]</code> 並不一定成立呢。</p>
<p>我們可以把上述的例外都分開測試。但 van Gasteren 與 Feijen 建議的作法是在陣列頭尾各補一個想像元素：<code>a[-1]</code> 小於任何數，<code>a[N]</code> 大於任何數。一個等價的說法是用這個<code>Φ</code>:</p>
<pre><code>Φ(i,j)  =  (i = -1  ∨  a[i] ≤ K)  ∧  (K < a[j]  ∨  j = N)</code></pre>
<p>最初令 <code>l, r := -1, N</code>, 那麼初始條件就滿足了。程式如下：</p>
<pre><code>  <span class="comment">{ 0 ≤ N ∧ Φ(-1,N) }</span>
  l, r := -1, N
  <span class="comment">{ Inv: -1 ≤ l < r ≤ N  ∧  Φ(l,r),   Bound: r - l }</span>
; do l+1 ≠ r →
    <span class="comment">{ l + 2 < r }</span>
    m := (l + r) / 2
  ; if a[m] ≤ K → l := m
    [] K < a[m] → r := m
    fi
  od
  <span class="comment">{ -1 ≤ l < N  ∧  Φ(l,l+1) }</span>
; if l > -1 → found := a[l] = K
  [] l = -1 → found := false
  fi
</code></pre>
<p>如果在陣列頭尾補元素讓你覺得很不妥，別擔心。迴圈不變量保證了 <code>-1 < m < N</code>，因此從頭到尾，<code>a[-1]</code> 和 <code>a[N]</code> 兩個位置都沒被讀過 -- 陣列 <code>a</code> 並不用真正有什麼改變，兩個想像元素只是為了證明演算法正確而假想出來的。這也讓我們可以處理空陣列了。我覺得這是 van Gasteren-Feijen 演算法漂亮之處。</p>
<h3>Bentley 的程式</h3>
<p>Jon Bentley 在 <a href="http://netlib.bell-labs.com/cm/cs/pearls/">Programming Pearls</a> 一書中給的二元搜尋法程式如果翻譯成 guarded command language, 並把陣列索引從 <code>[1..N]</code> 改成 <code>[0..N-1]</code>, 大約是這樣：<br />
<code>
<pre>
  l, r := 0, N-1
; do l ≤ r →
    m := (l + r) / 2
  ; if a[m] < K → l := m + 1
    [] a[m] = K → found := true; break
    [] K < a[m] → r := m - 1
    fi
  od
; found := false
</code></pre>
<p>Bentley 書中也有以口語描述的正確性證明。我原想在課堂上講解這個程式，畢竟這個版本流傳較廣 -- 許多函式庫裡的二元搜尋法程式就是這麼寫的。但困難之一是若要把 <code>K < a[m]</code> 和 <code>r := m - 1</code> 牽上關係，似乎非得提早用上陣列已排序好的性質。這個演算法便比較難在更廣泛的脈絡中解釋了。當然，另一個困難是我不知如何在 Hoare logic 中簡單地解釋 <code>break</code>。</p>
<p>乍看之下我以為 Bentley 演算法的搜尋範圍縮小得比 van Gasteren-Feijen 快：<code>l</code> 和 <code>r</code> 分別設為 <code>m+1</code> 與 <code>m-1</code>，並不是 <code>m</code>. 細看之後又發現並不盡然。變數 <code>l</code> 可設為 <code>m+1</code>, 因為 <code>m</code> 的位置已由另一個比較 <code>a[m] = K</code> 處理；變數 <code>r</code> 設為 <code>m-1</code>，則可能僅因為 Bentley 將陣列區段用 <code>a[l..r-1]</code> 表示，而 van Gasteren-Feijen 演算法把同一個區段表達為 <code>a[l..r)</code>.</p>
<p>Bentley 與 van Gasteren-Feijen 演算法解的是問題並不完全相同。當 <code>K</code> 在陣列中出現一次以上，Bentley 演算法不限定傳回哪個，van Gasteren-Feijen 則必定傳回索引最大的那個。當 <code>K</code> 不在陣列中時，van Gasteren-Feijen 演算法似乎較快，因為兩者都得跑完，而 van Gasteren-Feijen 的迴圈中只有一個比較（最後一個比較可省去）。Bentley 演算法若提早發現 <code>K</code> 可隨時跳出迴圈，代價是迴圈中多了一個比較。當陣列中確實找得到 <code>K</code>, 這樣的交換是否值得呢？<a href="http://penguin.ewu.edu/~trolfe/BinarySearch/index.html">Timothy J. Rolfe</a> 的實驗似乎認為只用一個比較的演算法平均上仍快一些。</p>
<h3>習題</h3>
<p>Van Gasteren 與 Feijen 建議的幾個習題中包括這個：假設陣列 <code>a[0..N)</code> 是一串嚴格遞增的數字緊接著一串嚴格遞減的數字，試用二元搜尋法找到陣列中的最大值。本問題中我覺得合理的假設是遞增和遞減數列均可能為空的，但 <code>a</code> 至少有一個元素：</p>
<pre><code>0 < N ⋀
(∃ M: 0 ≤ M < N :
  (∀ i,j : 0 ≤ i < j ≤ M : a[i] < a[j]) ⋀
  (∀ i,j : M ≤ i < j < N : a[i] > a[j]))
</code></pre>
<p>這剛好是<a href="http://portal.acm.org/citation.cfm?id=1039912">「最大密度區段」問題的演算法之一</a>需要的一個副程式。我記得當時也是先隨便寫寫，幾次寫不對才發現真是難。現在總算是知道一點理論背景，覺得安心多了。你要試試看嗎？</p>
<h3>中間索引怎麼取？</h3>
<p>關於中間值 <code>m := (l+r)/2</code>, 後來另有些其他故事。<a href="http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html">Google 的 Joshua Bloch</a> 發現當陣列夠大時，<code>l</code> 和 <code>r</code> 相加可能造成溢位。他可不是故意挑毛病，這個 "bug" <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5045582">是 Sun 抓到的</a>。他建議該這麼寫：</p>
<pre><code>int m = l + ((r - l) / 2);                          /* for Java/C/C++ */
int m = (l + r) >>> 1;                              /* for Java */
m = ((unsigned int)l + (unsigned int)r)) >> 1;      /* for C/C++ */
</code></pre>
<p>Bloch 的發現自然引起了不少<a href="http://news.ycombinator.com/item?id=1130463">討論</a>。有人認為這是整數型別的問題，而不是二元搜尋法的 bug. 有人質疑，討論 <code>int</code> 無法索引的陣列是否有意義？接下來可能一路談到組合語言定址法上頭。有人說，要造成索引溢位，陣列得有 4GB 大，你這輩子在 4GB 的陣列中作二元搜尋過嗎？有人便答，人家可是 Google 來的。Google 的陣列比常人的大上幾倍也不是不可能的唷。</p>
<p>如果往抽象的極端走，Roland Backhouse 在 <a href="http://as.wiley.com/WileyCDA/WileyTitle/productCd-0470848820.html">Program Construction: Calculating Implementations from Specifications</a> 書中則建議應該用 <code>m := (l + r - 1) / 2</code> -- 如此一來不管該語言的整數除法到底是無條件消去、四捨五入、或是無條件進入，算出來的 <code>m</code> 值都會是 <code>⌊(l + r)/2⌋</code>.</p>
<h3>參考資料</h3>
<ul>
<li>Roland Backhouse. <a href="http://as.wiley.com/WileyCDA/WileyTitle/productCd-0470848820.html">Program Construction: Calculating Implementations from Specifications</a>. John Wiley and Sons, 2003.</li>
<li>Jon Bentley. <a href="http://netlib.bell-labs.com/cm/cs/pearls/">Programming Pearls</a>, Second Edition. Addison-Wesley, 2000.</li>
<li>Joshua Bloch. <a href="http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html">Extra, Extra - Read All About It: Nearly All Binary Searches and Mergesorts are Broken</a>. Google Research Blog, June 02, 2006.</li>
<li>Netty van Gasteren and Wim Feijen. <a href="http://www.mathmeth.com/wf/files/wf2xx/wf214.pdf">The binary search revisited</a>. AvG127/WF214, 1995.</li>
<li>Timothy J. Rolfe. <a href="http://penguin.ewu.edu/~trolfe/BinarySearch/index.html">Analytic derivation of comparisons in binary search</a>. SIGNUM Newsletter, Vol. 32, No. 4 (Oct 1997), pp. 15-19.</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://www.iis.sinica.edu.tw/~scm/ncs/2010/03/binary-search-revisited-02/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>再看二元搜尋法 Binary Search（上）</title>
		<link>http://www.iis.sinica.edu.tw/~scm/ncs/2010/03/binary-search-revisited/</link>
		<comments>http://www.iis.sinica.edu.tw/~scm/ncs/2010/03/binary-search-revisited/#comments</comments>
		<pubDate>Fri, 05 Mar 2010 08:05:32 +0000</pubDate>
		<dc:creator>Shin</dc:creator>
				<category><![CDATA[計算算計]]></category>
		<category><![CDATA[Hoare 邏輯]]></category>
		<category><![CDATA[二元搜尋法]]></category>

		<guid isPermaLink="false">http://www.iis.sinica.edu.tw/~scm/ncs/?p=40</guid>
		<description><![CDATA[如果你自認對二元搜尋 (binary search) 夠熟悉了，卻沒讀過 Netty van Gasteren 和 Wim Feijen 的研究筆記 <a href="http://www.mathmeth.com/wf/files/wf2xx/wf214.pdf">The Binary Search Revisited</a>, 強烈建議你找時間看看。]]></description>
			<content:encoded><![CDATA[<p>如果你自認對二元搜尋(binary search, 大陸譯作折半搜索) 夠熟悉了，卻沒讀過 Netty van Gasteren 和 Wim Feijen 的研究筆記 <a href="http://www.mathmeth.com/wf/files/wf2xx/wf214.pdf">The Binary Search Revisited</a>, 強烈建議你找時間看看。</p>
<p>最近為了今夏的 FLOLAC 研習營開始找些資料。為了教些離開研習營後仍有用的東西，今年我想教 Hoare 邏輯與 Dijkstra 使用最弱前提(weakest precondition) 的程式建構法。對想寫好程式的人來說，這應是基礎知識，但大家對這些題目卻陌生得令人驚訝。</p>
<p>二元搜尋似乎是個該談到的例子：給一個排序好、長度為 <code>N</code> 的陣列，在 <code>O(log N)</code> 的時間內決定其中是否有某個值。資訊系學生在演算法課上一定學過二元搜尋。身經百戰的程式員寫個二元搜尋程式應該也不是難事吧？然而 Jon Bentley 在他有名的 <a href="http://netlib.bell-labs.com/cm/cs/pearls/">Programming Pearls</a> 一書中卻提到只有大約一成的專業程式員能第一次把這程式寫對。這一定要親身試試看才會相信。我抱著警覺心試了一次，寫了一個應該正確（畢竟我對這套方法算是比較熟了），但不怎麼漂亮的程式。請朋友寫寫看，也果然出現了一些常見的 bug. 看來這確實是個程式設計課堂上該講的好例子。</p>
<h3>迴圈、不變量、與界限</h3>
<p><a href="http://www.mathmeth.com/wf/files/wf2xx/wf214.pdf">Van Gasteren 和 Feijen</a> 在一篇 1995 年的研究筆記中釐清了大家常有的一個迷思：你認為你對二元搜尋很了解嗎？那您可知道，二元搜尋其實並不一定要用在排序好的陣列上？事實上，他們認為總是把二元搜尋類比為翻字典找字，反倒造成了一個教學盲點。</p>
<p>Van Gasteren 和 Feijen 先試解一個更廣的問題：給定整數 <code>M</code> 和 <code>N</code>, 和一個接受兩個整數參數的布林函數 <code>Φ</code>，已知 <code>M < N</code>，並知 <code>Φ(M,N)</code> 成立。<code>Φ</code> 另需滿足一些條件，待會兒再談。試寫一個程式，找兩個介於 <code>M</code> 和 <code>N</code> 之間、滿足 <code>Φ</code> 的相鄰整數。若寫成邏輯式子，程式執行完畢後需滿足：</p>
<pre><code>M ≤ l < N   ∧   Φ(l,l+1)</code></pre>
<p>這是 Van Gasteren 和 Feijen 的程式：</p>
<pre><code>  <span class="comment">{ M < N ∧ Φ(M,N) }</span>
  l, r := M, N
  <span class="comment">{ Inv: M ≤ l < r ≤ N  ∧  Φ(l,r),   Bound: r - l }</span>
; do l+1 ≠ r →
    <span class="comment">{ l + 2 < r }</span>
    m := (l + r) / 2
  ; if Φ(m,r) → l := m
    [] Φ(l,m) → r := m
    fi
  od
  <span class="comment">{ M ≤ l < N  ∧  Φ(l,l+1) }</span>
</code></pre>
<p>這裡的虛擬碼使用的是 Dijkstra 的 <a href="http://en.wikipedia.org/wiki/Guarded_Command_Language">Guarded Command Language</a>。多個變數可同時設值（如 <code>l, r := M, N</code>），<code>do</code> 相當於 <code>while</code> 迴圈。條件判斷 <code>if</code> 和一般程式語言不同之處是若不只一個條件成立，程式可任選一個分支執行。註解依照 Algol 系語言的傳統用大括號，但此處我們也把註解視作斷言 (assertion), 表示程式執行到此處一定會成立的條件。這是給人看的資訊，也是我們藉以證明程式正確的重要線索。</p>
<p>證明迴圈正確性的兩大關鍵是其<em>不變量 (loop invariant)</em>和<em>界限 (bound)</em>。本程式只有一個迴圈，不變量和界限註記在這一行:</p>
<pre><code><span class="comment">{ Inv: M ≤ l < r ≤ N  ∧  Φ(l,r),   Bound: r - l }</span></code></pre>
<p>「不變量」事實上並不是一個「量」，而是程式每次執行到迴圈進入點前必定會滿足的條件。此處不變量為 <code>M ≤ l < r ≤ N</code> 和 <code>Φ(l,r)</code>。變數 <code>l</code> 和 <code>r</code> 的初始值分別是 <code>M</code> 和 <code>N</code>. 依照假設，<code>M = l < r = N</code> 和 <code>Φ(l,r) = Φ(M,N)</code> 確實成立。因此第一次執行到迴圈之前，不變量確實是滿足的。</p>
<p>迴圈的進入條件是 <code>l+1 ≠ r</code>，和不變量中的 <code>l < r</code> 合起來看，意思是 <code>l</code> 和 <code>r</code> 不是相鄰的整數。因此<code>m := (l + r) / 2</code> 執行後，<code>m</code> 必定在 <code>l</code> 和 <code>r</code> 之間，但不等於 <code>l</code> 和 <code>r</code>。接下來的 <code>if</code> 敘述中，變數 <code>l</code> 會被設成 <code>m</code> 的先決條件是 <code>Φ(m,r)</code>，變數 <code>r</code> 會被設成 <code>m</code> 的先決條件是 <code>Φ(l,m)</code>。不論是哪種情形，執行完 <code>if</code> 敘述後，<code>Φ(l,r)</code> 一定還是成立。所以再回到迴圈開頭時，不變量仍被滿足。</p>
<p>長此以往，直到有一天剛好 <code>l+1 ≠ r</code> 不成立了，也就是說 <code>l</code> 和 <code>r</code> 已是相鄰的整數，和不變量合起來看，剛好我們要的 結束條件 <code>M ≤ l < N  ∧  Φ(l,l+1)</code> 就被滿足了！</p>
<p>只是還有兩個但書：首先，<code>if</code> 敘述中的 <code>Φ(m,r)</code> 和 <code>Φ(l,m)</code> 這兩個條件至少要有一個成立。這是 <code>Φ</code> 的另一個要求：</p>
<pre><code> Φ(l,r)  ∧  l < m < r   ⇒   Φ(l,m)  ∨  Φ(m,r)<span style="float:right">(*)</span></code></pre>
<p>其次，我們怎知道迴圈會有停下來的一天呢？這就是「界限」 <code>r-l</code> 處理的部份。由於 <code>M < N</code>, 我們知道 <code>r-l</code> 最初會是一個正整數。而如前所述，每次進入迴圈後，<code>m</code> 的值必定在 <code>l</code> 和 <code>r</code> 之間，但不等於 <code>l</code> 和 <code>r</code>。因此迴圈每執行一次，<code>r-l</code> 就變小一些。當 <code>r-l</code> 等於一時，迴圈就得停了。因此我們知道這迴圈不可能永遠執行下去。</p>
<p>以上便是該程式正確的大略證明。我們只是用中文說說，更仔細的證明是應該要有些數學演算的。關於什麼算是證明、為何要有「形式」證明，希望以後有機會寫寫看。</p>
<p>有哪些函數 <code>Φ</code> 滿足條件 <code>(*)</code> 呢？ Van Gasteren 與 Feijen 舉的例子包括:</p>
<ul>
<li><code>Φ(i,j) = a[i] ≠ a[j]</code>，此處 <code>a[M..N]</code> 是某陣列。程式會幫我們找到兩個相鄰但不相等的元素。Van Gasteren 與 Feijen 認為這個特例可能是解說二元搜尋比較合適的例子。</li>
<li><code>Φ(i,j) = a[i] × a[j] ≤ 0</code>, 程式將找到兩個相鄰但正負號不相等，或至少有一個為零的元素。</li>
<li><code>Φ(i,j) = a[i] < a[j]</code>, </li>
<li><code>Φ(i,j) = a[i] ∨ a[j]</code>, 等等。</li>
</ul>
<p>再回到最初的問題：怎麼在排序好的陣列中找某個關鍵值呢？理想上我們希望設計某個 <code>Φ</code>，然後套用上面的程式。先賣個關子，下回分解。 <img src='http://www.iis.sinica.edu.tw/~scm/ncs/wp/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley' /> </p>
<h3>參考資料</h3>
<ul>
<li>Roland Backhouse. <a href="http://as.wiley.com/WileyCDA/WileyTitle/productCd-0470848820.html">Program Construction: Calculating Implementations from Specifications</a>. John Wiley and Sons, 2003.</li>
<li>Jon Bentley. <a href="http://netlib.bell-labs.com/cm/cs/pearls/">Programming Pearls</a>, Second Edition. Addison-Wesley, 2000.</li>
<li>Joshua Bloch. <a href="http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html">Extra, Extra - Read All About It: Nearly All Binary Searches and Mergesorts are Broken</a>. Google Research Blog, June 02, 2006.</li>
<li>Netty van Gasteren and Wim Feijen. <a href="http://www.mathmeth.com/wf/files/wf2xx/wf214.pdf">The binary search revisited</a>. AvG127/WF214, 1995.</li>
<li>Timothy J. Rolfe. <a href="http://penguin.ewu.edu/~trolfe/BinarySearch/index.html">Analytic derivation of comparisons in binary search</a>. SIGNUM Newsletter, Vol. 32, No. 4 (Oct 1997), pp. 15-19.</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://www.iis.sinica.edu.tw/~scm/ncs/2010/03/binary-search-revisited/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>程式設計的公設基礎：四十年後</title>
		<link>http://www.iis.sinica.edu.tw/~scm/ncs/2009/11/40-years-after-axiomatic-basis-for-programming/</link>
		<comments>http://www.iis.sinica.edu.tw/~scm/ncs/2009/11/40-years-after-axiomatic-basis-for-programming/#comments</comments>
		<pubDate>Sun, 08 Nov 2009 12:20:05 +0000</pubDate>
		<dc:creator>Shin</dc:creator>
				<category><![CDATA[人物]]></category>
		<category><![CDATA[計算算計]]></category>
		<category><![CDATA[C.A.R Hoare]]></category>
		<category><![CDATA[Hoare 邏輯]]></category>
		<category><![CDATA[歷史]]></category>
		<category><![CDATA[邏輯]]></category>

		<guid isPermaLink="false">http://www.iis.sinica.edu.tw/~scm/ncs/?p=29</guid>
		<description><![CDATA[1969 年，<a href="http://en.wikipedia.org/wiki/C._A._R._Hoare">C.A.R. Hoare</a> 在 Communication of the ACM (12(10):576–580) 發表論文 <a href="http://www.eecs.berkeley.edu/~necula/Papers/HoareAxioms.pdf">An Axiomatic Basis for Computer Programming</a>，在文中提出後來被稱作 <a href="http://en.wikipedia.org/wiki/Hoare_logic">Hoare 邏輯</a>的一套公設系統, 被認為是計算科學史上最重要的幾篇論文之一。時間轉眼間過去了，今年已經是這篇論文的四十歲生日。]]></description>
			<content:encoded><![CDATA[<p>1969 年，<a href="http://en.wikipedia.org/wiki/C._A._R._Hoare">C.A.R. Hoare</a> 在 Communication of the ACM (12(10):576–580) 發表論文 <a href="http://www.eecs.berkeley.edu/~necula/Papers/HoareAxioms.pdf">An Axiomatic Basis for Computer Programming</a>，在文中提出後來被稱作 <a href="http://en.wikipedia.org/wiki/Hoare_logic">Hoare 邏輯</a> (Hoare Logic) 的一套公設系統, 被認為是計算科學史上最重要的幾篇論文之一。時間轉眼間過去了，今年已經是這篇論文的四十歲生日。最近 Hoare 應 Communications 之邀，<a href="http://cacm.acm.org/magazines/2009/10/42360-retrospective-an-axiomatic-basis-for-computer-programming/fulltext#FNA">回顧後來的發展</a>。有哪些是他預期的，哪些是他沒料到的？</p>
<p>1960-68 年間，Hoare 仍在業界工作，帶領一個實作 <a href="http://en.wikipedia.org/wiki/ALGOL">ALGOL 60</a> 編譯器的團隊。ALGOL 的語法用 context-free language 定義得相當精簡優雅，語意卻仍以非形式的方式定義。Hoare 希望有個方式能一方面嚴格描述 ALGOL 的語意，一方面又不限制到編譯器製作者使用各種最佳化方法的自由。這是 Hoare 邏輯的由來。</p>
<p>Hoare 漸漸發現這題目大概可以研究一輩子。而且大概直到他退休，這東西也不會在業界得到廣泛應用。這使他在 1968 年從業界進入學界，而到他 1999 年退休為止，看來這兩個預測都蠻準確的。</p>
<p>令他意外的事情呢？Hoare 說他可以預期程式會越來越大，測試這種大程式也會越來越困難。他沒想到的是測試不僅是用在程式上，更是對人的考驗。通過這種考驗的工程師所培養出的直覺和經驗也是讓軟體可靠的重要因素。形式方法應該去支持程式員的直覺判斷，而非壓抑它。況且開發中的軟體若在測試中表現得很差，剛好可讓開發部門作為說服管理部門延長開發時間的依據。</p>
<p>把測試和證明對立起來的看法也許是個基本錯誤 &#8212; 工程師本來就要使用各種有用的工具。形式方法能做的貢獻是盡量提供更好的工具。在他退休並加入微軟劍橋研究中心後，他驚訝地發現 Hoare 邏輯斷言並不是用來證明程式正確性，而是用在測試並發現錯誤上。形式方法在除錯上的應用反倒比證明來得早。</p>
<p>Hoare 的邏輯斷言使用的是標準的邏輯與離散數學。我想後來許多程式語言學者的想法也是盡量設計符合數學模型的程式語言。Hoare 卻說近年來的發展剛好相反：許多數學概念為了分析程式語言的新功能（如物件、classes, heap, 指標等）而發展出來；大家開發新種類的代數，用來描述分散系統、並行系統等等。新的邏輯，如線性邏輯、分離邏輯等為了分析程式的行為而發明，有些還在其他的領域，如計算生物學、基因學、甚至社會學派上用場。</p>
<p>學者們一度相信要等到軟體錯誤造成了大災害，大家才會了解形式方法與軟體驗證的重要。事實卻不然。1996 年 Ariane V 火箭測試發射因軟體故障而失敗後，管理階層的策略是跑更多更嚴密的測試。越嚇他們，他們越會抱緊熟悉的方法不放。對軟體驗證的興趣反倒是從利潤出發：有效地使用形式方法會比較省錢。</p>
<p>而使軟體驗證流行的助力卻來自大家都沒想到的地方：駭客們。駭客攻擊造成大量的金錢損失，而駭客使用的軟體漏洞通常無法經由測試找到。唯一的可能是自動分析並驗證程式碼。這使得資訊安全成為軟體驗證的一大賣點，也由此漸漸推廣到汽車、電子產品、和航太產業中。</p>
<p>Hoare 接著談到業界與學界的不同使命。業界自然是使用最成熟的技術、解決最容易而迫切的問題。但學界的天職是完全相反的：建構最泛用的理論、解釋最多的現象、尋求可長可久的知識。不論哪種科學研究都嘗試回答這一系列的問題：這個東西做什麼？怎麼運作？為什麼能這麼運作？而有什麼證據顯示我們提出的答案是對的？在電腦科學中我們已經大致知道怎麼回答這些問題。一個程式的規格告訴我們它做什麼，斷言和其他內部模組、界面間的約定告訴我們他怎麼運作，程式語言語意解釋它為什麼能這麼運作，數學與邏輯證明增強我們的信心，而現在這些證明可以用電腦和工具產生和檢查。現在電腦科學家的舞台與使命是從事有說服力的實驗，檢驗這些工具和理論是否足以涵蓋今日大部分的程式、設計模式、語言、和應用。「實驗」在此的意義可能是現有應用程式的重新設計，從這些實驗得到的經驗將繼續推動理論與工具的進步。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.iis.sinica.edu.tw/~scm/ncs/2009/11/40-years-after-axiomatic-basis-for-programming/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>

