17
導讀說明 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地 讀完《導讀說明》,還要時不時地在學習過程中重讀這些文字,以確保自己不會因為糾纏於 書中的一些細節而變得捨本逐末,甚至偏離了預定的學習航線。切記! 「覆蓋面廣,點到為止,注重程式碼」是本書的最大特點,而這 3 個特點都是為了向業界 靠攏,注重廣度而非深度。為了方便練習,書中所有題目來自於 UVa 線上評測系統和 ACM/ICPC 真題庫(Live Archive, LA)。雖然這兩個題庫的題目不能包羅萬象,但對於「點 到為止」來說綽綽有餘。書中的程式碼更多是為了提供一個容易理解、適合比賽的參考實 例,並不推薦讀者直接使用甚至背誦它們(關於這一點,在稍後還會有詳細敘述)。 關於知識點 本書不是一本演算法競賽入門類圖書,因此並不從程式設計語言、演算法的概念、基礎資 料結構、漸進時間複雜度分析這些內容講起。如果你是一個新手,建議先閱讀《提升程式 設計的邏輯思考力-國際程式設計競賽之演算法原理、題型、解題技巧與重點解析》(和本 書搭配著閱讀也可以)。 聰明的選手都是善於利用網路資源的,如在網上交流討論、閱讀高手的部落格,尋找解題 報告和測試資料等。這些方法也是閱讀本書的重要輔助手段。由於演算法競賽涉及的知識 點非常廣,並且讀者所具備的知識水準參差不齊 註1 ,因此,不同讀者在閱讀本書時會遇到 不同的困難。這些困難有時並不是讀者的問題,而且也無法靠修改書稿來避免 註2 ,因此, 最好的辦法就是上網搜索! 註1 筆者認為,本書的讀者至少會覆蓋從小學高年級學生到從事 IT 工作多年的專案工程師。 註2 例如,有些術語在學術界並沒有統一的譯名或名稱,或者在不同文獻中的譯名或名稱不同,如果本書採用的名稱和你 所熟知的不同,反而會產生一些困擾。

導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

  • Upload
    others

  • View
    1

  • Download
    0

Embed Size (px)

Citation preview

Page 1: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

導讀說明

如何閱讀本書

歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

讀完《導讀說明》,還要時不時地在學習過程中重讀這些文字,以確保自己不會因為糾纏於

書中的一些細節而變得捨本逐末,甚至偏離了預定的學習航線。切記!

「覆蓋面廣,點到為止,注重程式碼」是本書的最大特點,而這 3 個特點都是為了向業界

靠攏,注重廣度而非深度。為了方便練習,書中所有題目來自於 UVa 線上評測系統和

ACM/ICPC 真題庫(Live Archive, LA)。雖然這兩個題庫的題目不能包羅萬象,但對於「點

到為止」來說綽綽有餘。書中的程式碼更多是為了提供一個容易理解、適合比賽的參考實

例,並不推薦讀者直接使用甚至背誦它們(關於這一點,在稍後還會有詳細敘述)。

關於知識點

本書不是一本演算法競賽入門類圖書,因此並不從程式設計語言、演算法的概念、基礎資

料結構、漸進時間複雜度分析這些內容講起。如果你是一個新手,建議先閱讀《提升程式

設計的邏輯思考力-國際程式設計競賽之演算法原理、題型、解題技巧與重點解析》(和本

書搭配著閱讀也可以)。

聰明的選手都是善於利用網路資源的,如在網上交流討論、閱讀高手的部落格,尋找解題

報告和測試資料等。這些方法也是閱讀本書的重要輔助手段。由於演算法競賽涉及的知識

點非常廣,並且讀者所具備的知識水準參差不齊註1,因此,不同讀者在閱讀本書時會遇到

不同的困難。這些困難有時並不是讀者的問題,而且也無法靠修改書稿來避免註2,因此,

最好的辦法就是上網搜索!

註1 筆者認為,本書的讀者至少會覆蓋從小學高年級學生到從事 IT 工作多年的專案工程師。 註2 例如,有些術語在學術界並沒有統一的譯名或名稱,或者在不同文獻中的譯名或名稱不同,如果本書採用的名稱和你

所熟知的不同,反而會產生一些困擾。

Page 2: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

提升程式設計的解題思考力

- x -

本書是完全的題目導向,掌握了書中的所有例題,也就掌握了書中的主要知識點和方法、

技巧。在習題中也有少量相對不那麼重要、例題沒有涉及的東西,在每章的小結部分會明

確指出。換句話說,對於每個知識點,最好的方法是結合例題分析和程式碼去理解,上機

實作,最終達到能獨立解決同類問題的目的。

本書的學習並不需要依照順序,但最好先閱讀第 1 章,因為其中不少思考方式和編寫程式

碼技巧適用於全書。接下來,數學(第 2 章)、資料結構(第 3 章)、幾何(第 4 章)、圖論

(第 5 章)等內容可以根據讀者需要按任意順序進行學習。事實上,筆者建議大家先略讀

學習這些章節的重要內容,等對全書有了一個整體認識之後再精讀精練,千萬不要過早地

陷入難懂的細節(每章都有一些相對難懂的內容)。第 6 章主要是一些零散的知識點和技

巧、方法,可以與其他章節穿插閱讀。

請重視每章後面的小結與習題部分。雖然每章前面的文字一視同仁地對所有內容逐一講解,

但在演算法競賽中,這些內容並不是同等重要的,思維訓練和程式設計訓練的內容和比重

也不盡相同。當你不知道接下來應該學些什麼、練些什麼時,這些內容會是你最好的幫手。

例如,如果關於某個知識點的習題特別少,這通常意味著該知識點很少在競賽中出現,或

者只需要很少的練習就能掌握到其精髓註3。

如何做題目

書中的重要題目(包括例題和習題)均配有完整程式碼(限於篇幅,這些程式碼不一定會

在書中出現)和測試資料,以方便讀者學習,而其他題目也大都附有中文翻譯,並且指明

了題號、提交人數和通過比例等統計資料,以供讀者訓練參考。

書中題目數量不少,因此做題目時必然要有一個先後順序,建議初學者優先解決那些通過

人數較多的題目,而已經開始專項訓練的讀者則可以根據需要選擇難一些的題目。

考慮到很多讀者並不是「久經沙場」的老手,接下來以「螞蟻」例題來當作實例,介紹做

題目的一般步驟。

註3 這兩個原因並不是孤立的。一般來說,競賽會偏愛那些精巧、靈活的內容,而非那些很難變形、擴充的死板東西。

Page 3: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

導讀說明

- xi -

》螞蟻(Piotr's Ants, UVa 10881)

一根長度為 L 釐米的木棍上有 n 隻螞蟻,每隻螞蟻會朝左爬或朝右爬,速度為 1 釐米/秒。

當兩隻螞蟻相撞時,二者同時掉頭(掉頭時間忽略不計)。列出每隻螞蟻的初始位置和朝

向,計算 T 秒之後每隻螞蟻的位置。

輸入格式

輸入第一行為資料組數。每組資料的第一行為 3個正整數 L, T, n(0 ≤ n ≤ 10 000),以下 n行每行描述一隻螞蟻的初始位置。其中,整數 x 為它距離木棍左端的距離(單位:釐米),字母表示初始朝向(L 表示朝左,R表示朝右)。

輸出格式

對於每組資料,輸出 n行,按輸入順序列出每隻螞蟻的位置和朝向(Turning表示正在碰撞)。在第 T秒之前已經掉下木棍的螞蟻(正好爬到木棍邊緣的不算)輸出 Fell off。

樣例輸入

2 10 1 4 1 R 5 R 3 L 10 R 10 2 3 4 R 5 L 8 R

樣例輸出

Case #1: 2 Turning 6 R 2 Turning Fell off Case #2: 3 L 6 R 10 R

這是第 1 章中的例題 5,是一道很有意思的題目。題目正文只有 3 句話,並不難理解,但

正文並不是題目的全部。可以看到,一道完整的題目至少包含 3 個部分:題目描述、輸入

輸出格式和樣例輸入輸出,缺一不可註4。

題目描述可以讓你瞭解到你需要解決一個什麼樣的問題,但有些細節可能不會涉及。在第

一次閱讀時可以忽略不明白的地方,直接看輸入輸出格式,以瞭解具體的輸入輸出方法。

對照輸入輸出格式和題目描述,可以更清楚地知道這道題目已知什麼,要求什麼。

註4 有的題目還包含其他部分,比如背景介紹、樣例解釋,甚至解題提示,但這些都不是必需的。

Page 4: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

提升程式設計的解題思考力

- xii -

輸入輸出格式往往有規律可循。例如,本書的題目全部採用 ACM/ICPC 比賽題目的格式,

採用多資料登錄輸出。最常見的格式有以下兩種。

格式一:輸入第一行包含資料組數 T。每組資料的第一行包含……主程式通常這樣編寫:

int main() { scanf("%d", &T); while(T--) { input(); //讀取單組數據 solve(); //求解問題 output(); //輸出 } return 0; }

格式二:輸入包含多組資料。每組資料的第一行包含一個整數 n,輸入結束旗標為 n=0。

主程式通常這樣編寫:

int main() { while(scanf("%d", &n) == 1) { if(n == 0) break; //讀取其他資料,求解並且輸出 ... } return 0; }

注意,上述程式碼用到了 scanf 的返回值,它返回的是成功讀取的元素數目。換句話說,

即使真實資料的最後並沒有 n=0 的「旗標」,上述程式也會在檔案結束後退出主迴圈。當

然,正常情況下命題者不會忘記在資料末尾加上這個「旗標」。但人非聖賢,孰能無過,即

使在 ACM/ICPC 世界總決賽這樣的重量級比賽中,也曾出現過資料不符合題目描述的情

況。因此,作為選手來說,最好的方法是編寫一個儘量強固的程式,即使在資料有瑕疵的

情況下仍然能夠通過資料註5。

輸入輸出格式的另一個作用是告知資料範圍和限制。比如上述題目中,「n ≤ 10,000」這個

限制實際上是在警告選手:O(n2)時間複雜度的演算法也許會超時註6。另一些重要的限制

包括「輸出保證不超過 64 位元不帶正負號的整數的範圍」、「輸入檔大小不超過 8MB」。前

者通常意味著我們不必使用高精度整數註7,而後者通常意味著我們需要注意輸入資料的時

間,不要採用太慢的輸入方法(這個問題會在第 1 章中詳細討論)。

註5 書中多次強調的“把陣列開大一些”也是基於這個考慮。 註6 多數情況如此,但如果常數特別小,O(n2) 時間演算法也有可能通過 n=10000 的資料。 註7 但如果演算法糟糕或者實現不好的話,中間結果有可能溢出!

Page 5: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

導讀說明

- xiii -

在很多題目中,正確的輸出並不是唯一的。在這種情況下,題目會明確指出多解時任意輸

出一個解即可(此時會有一個稱為 special judge 的程式來判斷你的解是否正確),或告訴你

輸出哪一組解,以確保輸出的唯一性。多數情況都是輸出字典序最小或者最大的解。

看完題目描述和輸入輸出格式之後,還有一件重要的事要做,那就是閱讀樣例。很多題目

的樣例都是「可讀」的,即至少包含一個簡單到能夠手算出來的例子。在這種情況下,強

烈建議讀者手算一下這個樣例,因為這不僅可以幫助你理解題意、消除潛在的歧義,還能

啟發你的思路,使你從手算的過程中概括整理出本題的通用解法。如果有疑問,還可以很

方便地向主辦方提出註8。

接下來,就可以設計演算法並編寫程式了。如何設計演算法?如何編寫程式?這正是本書

的主題,所以在這裡不再贅述,假定你已經順利地寫完程式。

接下來應當測試,看看你的程式是不是正確。程式設計的一大特點是「失之毫釐,謬以千

里」,因此,哪怕只是寫錯了一個運算子,程式的結果也可能完全不對。所以,在提交程式

之前應當好好測試一下。測試什麼資料呢?最現成的當然就是樣例了,但即使這樣也遠遠

不夠。很多時候,樣例並不具有典型性,通過樣例也許只是碰巧;如果題目很難,就算樣

例很典型,也未必能覆蓋到所有情況。也許你的程式幾乎是完美的,但在一些特殊情況下

也會出錯,而樣例並不包含這些特殊情況。因此,進行全面的測試是很有必要的。

一旦測試出錯,就需要除錯(debug)你的程式。除錯的方法有很多種,如追蹤除錯,即利

用 IDE 的單步、中斷點、watch 等功能,動態地檢查程式的執行過程是否和預想中的一樣。

這樣的技巧固然有用,但在演算法競賽中更常見的還是「斷言 +輸出中間結果」的方法,

這在《提升程式設計的邏輯思考力》一書中已有詳述。需要特別注意的是,修改程式後最

好測試一下以前測過的資料,以免「拆東牆補西牆」,修改了老 bug 的同時又引入了新 bug。

不管是參加什麼樣的比賽,都有一個「提交程式碼」的過程註9。為了盡可能地避免一些「非

正常因素」的干擾,進行一些提交前的檢查還是很有必要的。主要包括檢查輸入輸出管道

是否正確(例如,檔案輸入還是標準輸入,有沒有忘記關閉檔案)、除錯用的中間結果是否

已被遮罩、陣列有沒有開得足夠大、輸出中的常數字串有沒有打錯(一般來說,Yes 打成

yes 將被判錯)等。

註8 如果你問:「這題是什麼意思?」,通常不會有人回答;如果你問「這個樣例為什麼輸出 1」,通常都能得到滿意的答案。 註9 有的比賽是上機結束後由機器自動收取,沒有明顯的「提交」過程,在此情況下,請把「提交」理解成「比賽結束」。

Page 6: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

提升程式設計的解題思考力

- xiv -

本書的題目風格均為 ACM/ICPC 式的,因此每個程式只有「對」和「不對」兩種結果(當

然,還會告知「不對」的原因,詳見《提升程式設計的邏輯思考力》),而沒有「半對半錯」

之說。好在本書的所有題目均選自 UVa 線上評測系統和 ACM/ICPC 真題庫(Live Archive,

簡稱 LA),這兩個線上題庫的操作幾乎一樣,都可以很方便地提交題目(只需要知道題號,

通過網站左下方功能表中的「Quick Submit」命令即可提交)。在完成書後習題時,常常需

要參考原題(如看樣例或者輸入輸出格式),這裡介紹兩個「快捷方式」(以題目 12345 和

2345 為例):

UVa 題目連結:http://uva.onlinejudge.org/external/123/12345.html

LA 題目連結:http://livearchive.onlinejudge.org/external/23/2345.html

當然,還可以直接用搜尋引擎搜索「UVa 12345」或者「livearchive 2345」。

為了方便讀者,書中列出了所有例題和習題的提交統計資訊,比如 511/80%是指有 511 個

使用者提交,其中約 80%的使用者通過。

請注意,題號越大,說明加入題庫的時間越晚。新加入題庫的題目,其提交量不會很大,

因此統計資料並不一定能客觀反映其真實難度。

很多選手都有過「一道題折騰了一個星期,交了 100 次才過」的經歷,可見,在演算法競

賽中,堅韌不拔的精神是多麼地重要。當然了,筆者並不建議讀者每道題目都錯上 100 次

以後才罷手,因此提供了重要題目的資料和程式碼。畢竟,在初學階段,學習他人的程式

碼以及「對著評測資料除錯」都是重要的學習方法。但在達到一定水準之後,最好是獨立

編寫程式,並且不借助評測資料——要知道,在真實比賽時,你能拿到的所有資料僅僅是

樣例。

關於範例程式碼

本書中有很多程式碼,如果善加利用,這些程式碼可以幫你很大的忙;但如果濫用,非但

不能發揮它們的最大好處,還可能會起到不好的作用。

關於書中的範例程式碼,筆者的建議只有一點:不要直接使用。理由有如下 3 點。

第一,不理解的程式碼不好用。所有程式碼都有它自身的適用範圍,如果不理解其中的原

理,不但有可能錯誤地使用這些程式碼,而且一旦需要對這些程式碼進行一定的修改才能

Page 7: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

導讀說明

- xv -

符合題目要求時,你就會束手無策。解決方法很簡單:想用的程式碼,事先重寫一遍。這

樣不僅能加深你的理解,用的時候也更放心。

第二,程式碼習慣因人而異。書中的程式碼符合筆者的思維習慣和編碼風格,卻不一定適

合你。例如,書中的程式碼把某個東西從 0 開始編號,而你習慣把任何東西從 1 開始編號,

則當你混用自己的程式碼和書中的程式碼時,不僅程式設計時必須小心翼翼,而且很容易

出現一些難以找到的 bug。

第三,程式碼風格與傳統的專案程式碼有衝突。身為從事軟體開發多年的工程師,筆者深

知演算法競賽程式碼和專案程式碼的差異。在演算法競賽中,由於時間緊迫,很多東西都

「從簡」了,很多「規矩」也都被無視了。例如,演算法競賽中經常使用全域變數、記憶

體往往是事先分配一大堆而不是按照需求分配、很少使用 OO 特性(頂多用幾個帶有成員

函數的 struct,所有成員都是 public 的,並且很少用繼承)、單個函數通常比專案程式碼長,

但程式碼總長度通常更短,識別字通常也更短……如果本書採用傳統的專案風格來編寫程

式碼,不僅會讓書的厚度成倍增加,還會讓讀者在閱讀了大量「無關緊要」的程式碼之後

仍然不得要領,找不到最關鍵、最核心的地方。因此,筆者寧可讓程式緊湊些,一方面讓

讀者很容易抓住問題的核心,另一方面也鍛煉了讀者對程式整體邏輯(而非表達方式)的

「感覺」——這恰好是很多程式設計師所缺乏的註10。一旦真正理解了這些「緊湊程式碼」,

將其改成專案程式碼並不是難事。如前所述,這些程式碼會更好用,更符合你的需要。

不過,筆者有一點需要澄清:雖然和傳統的專案程式碼有如此多的差異,但這並不是說本

書的程式碼風格完全不適合專案項目。相反地,本書的寫作目的之一是希望讀者能把兩種

風格結合起來。例如,演算法競賽的選手寫出來的程式碼通常比較簡潔,這使程式具有更

好的可讀性和易維護性。筆者不主張把可讀性膚淺地理解成「變數名取得長且有意義」、

「注釋足夠多」、「每個函數都不超過 10 行」以及「擁有一個看上去很棒的類別層次結構」,

而應該更加看重程式的邏輯關係和執行流程。例如,對於下面這一段程式碼:

for(int i = 0; i < n; i++) for(int j = i+1; j < n; j++) if(a[i]>a[j]) { int t = a[i]; a[i] = a[j]; a[j] = t; }

任何一個具備一定演算法基礎的程式師都能夠毫不猶豫地說出它的作用,不需要任何注釋,

也不需要給任何變數改一個「更有意義」的名字;相反,很多工整、乾淨甚至看起來很優

美的「專案型」的演算法程式碼,卻用了 1,000 行來完成 100 行就能搞定的功能註11。

註10 如果你在進行逆向工程,面對的是大量二進位的機器碼,不僅沒有注釋,連變數名都沒有,所能把握的就只有邏輯了。 註11 筆者一點都沒有誇張。經驗表示,10 倍的比例是很正常的。

Page 8: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

提升程式設計的解題思考力

- xvi -

隨著程式碼長度的增加,潛在的 bug 數量、配套的單元測試數量以及人力成本等都會隨之

增長,而且增速通常快於線性函數。在這樣的情況下,採用競賽式的風格編寫專案項目中

的演算法程式碼不僅是可行的,也是值得提倡的註12。如果認真體會本書中的程式碼,你

會發現它們比傳統專案程式碼短的主要原因並不是變數名短、函數分得不細,而是因為邏

輯更簡單、清楚、直接。

考慮到參賽語言限制(ACM/ICPC 只能使用 C、C++和 Java,NOI 支援 Pascal,但不支援

Java),本書的正文選擇了兩種競賽都支援的 C++。但由於筆者在工作中還使用了大量的非

C++語言(包括 Java、C#、Python、JavaScript/CoffeeScript、ActionScript、Erlang、Scala 等),

因此,我們特別編寫了一個附錄來簡單介紹其他 3 種語言——Java、C#和 Python。強烈建

議讀者反復閱讀這個特別的附錄,它不僅能開闊你的眼界,還能幫你在競賽和真實的軟體

開發之間架起一座橋樑。演算法是軟體開發的基石,但不是全部。

註12 也許最大的問題是:如果仍然以 LOC(程式碼行數)來衡量工作量,習慣於編寫緊湊程式碼的程式設計師往往很吃虧。

Page 9: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

演算法程式競賽雖不是數學競賽,但它是一門離不開數學的競賽。它涉及組合數學、數論、

機率論、抽象代數、線性代數、微積分、遊戲論等領域,要求選手有較為全面的數學基礎。

本章力圖介紹演算法競賽中常用的數學知識點與思考方法,並透過題目和程式碼加深讀者

的理解。

Chapter 02

數學基礎

Page 10: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

提升程式設計的解題思考力

- 2-2 -

2.1 基本計數方法

計數方法的兩個 基礎的原理是加法原理和乘法原理。

加法原理(Addition Principle)。做一件事情有 n 個辦法,第 i 個辦法有 pi 種方案,則一共

有 p1+p2+…+pn 種方案。乘法原理(Multiplication Principle)。做一件事情有 n 個步驟,第 i

個步驟有 pi 種方案,則一共有 p1p2…pn 種方案。

乘法原理是加法原理的特殊情況(按照第一步驟進行分類),二者都可用於遞推。注意應用

加法原理的關鍵是分類,各類別之間必須沒有重複、沒有遺漏。如果有重複,可以使用容

斥原理,如下所述。

容斥原理(Inclusion-Exclusion Principle,或譯排容原理)。假設班裡有 10 個學生喜歡數學,

15 個學生喜歡語文,21 個學生喜歡程式設計,班裡一共有多少個學生呢?是 10+15+21=46

個嗎?不是的,因為有些學生可能同時喜歡數學和語文,或者語文和程式設計,甚至還有

可能三者都喜歡。為了敘述方便,我們把喜歡語文、數學、程式設計的學生集合分別用 A,

B, C 表示,則學生總數等於 |A∪B∪C|。剛才已經講過,如果把這三個集合的元素個數 |A|、

|B|、|C| 直接加起來,會有一些元素重複統計,因此需要扣掉 |A∩B|、|B∩C|、|C∩A|,但這

樣一來,又有一小部分多扣了,需要加回來,即 |A∩B∩C|。這樣,我們就得到一個公式:

|A∪B∪C|=|A|+|B|+|C|-|A∩B|-|B∩C|-|C∩A|+|A∩B∩C|

一般來說,對於任意多個集合,我們都可以列出這樣一個等式,等式左邊是所有集合的並

的元素個數,右邊是這些集合的「各種搭配」。每個「搭配」都是若干個集合的交集,且每

一項前面的正負號取決於集合的個數——奇數個集合為正,偶數個集合為負。事實上,第

1 章例題 24「廢料堆」中的"加加減減"也符合這個規律。

容斥原理有一個變種:假設全集為 S,另有 3 個集合 A, B, C,不屬於 A、B、C 任何一個集

合,但屬於全集 S 的元素一共有多少個呢?和前面的方法類似,我們首先扣除 A, B, C,然

後把 |A∩B|、|B∩C| 和 |C∩A| 加回來, 後再扣掉多加的 |A∩B∩C|。

容斥原理還有其他變種,但思考方式萬變不離其宗:都是加加減減,把重複的扣掉,再把

扣多的加回來。如果很難一次想清楚,可以先推導兩個集合或者 3 個集合的例子,然後深

入思考。

Page 11: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

Chapter 02 數學基礎

- 2-3 -

下面列舉幾個常見的計數問題。

問題 1:排列問題。有 n 個不同的數,選 k 個排成一排,每個數 多選一次,問有多少種排

列方法?

分析:記答案為 P(n, k)。由乘法原理,每個步驟選一個數,第 1 個步驟有 n 種選擇,第 2 個

步驟有 n-1 種選擇(不管第 1 步選了什麼),…,第 k 個步驟有 n-k+1 種選擇,所以 P(n, k)

= n(n-1)(n-2)…(n-k+1)。用階乘表示就是 P(n, k)=n!/(n-k)!。特別地,n 個數的全排列 P(n, n)=n!

問題 2:組合問題。有 n 個不同的數,選出 k 個(順序無關),每個數 多選一次,問有多

少種選法?

分析:記答案為 C(n, k)。把 n 選 k 的排列問題看成兩個步驟:首先選出 k 個數的組合,然後

把這 k 個數進行全排列。由乘法原理知 P(n, k)=C(n, k)×P(k, k),因此 C(n, k) = P(n, k)/P(k, k)

= n!/((n-k)!k!)。

C(n,k)在組合計數中佔有極重要的地位。常用性質如下。

性質 1:C(n, 0)=C(n, n)=1

性質 2:C(n, k)=C(n, n-k)

證明:選了 k 個數以後剩下的數恰好有 n-k 個,因此選 k 個和選 n-k 個的方案一一對應。

性質 3:C(n, k)+C(n, k+1)=C(n+1, k+1)

這是組合數的遞推公式,經常用於預處理,證明如下:n+1 個數裡選 k+1 有兩類辦法。要麼

選第 1 個數,要麼不選第 1 個數。如果不選,則問題轉化為 n 個數裡選 k+1 個數;如果選,

則問題轉化為 n 個數裡選 k 個數。這兩類辦法是不重複不遺漏的,由加法原理得證。

問題 3(二項式展開):求(a+b)n 展開式的各項係數。

分析:根據二項式定理

0

( )n

n k n k kn

k

a b C a b−

=

+ =

只需求出所有的 C(n, k)。不管是用定義(階乘相除)還是性質 3,時間複雜度都是 O(n2)。

有沒有辦法算得更快呢?答案是肯定的。這需要用到下面的性質 4。

性質 4:C(n,k+1)=C(n,k)×(n-k)/(k+1)

證明:直接利用公式

C(n,k+1)=n(n-1)(n-2)…(n-k+1)(n-k)/(k+1)!

C(n,k)=n(n-1)(n-2)…(n-k+1)/k!

Page 12: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

提升程式設計的解題思考力

- 2-4 -

兩式相除得 C(n, k+1)/C(n, k)=(n-k)/(k+1)。

有了這個性質,從 C(n, 0) 開始遞推,只需要 O(n) 時間就能求出所有的 C(n, 0), C(n, 1), C(n,

2), …, C(n, n)。注意不要讓運算過程中出現乘法溢出。

問題 4:有重複元素的全排列。有 k 個元素,其中第 i 個元素有 ni 個,求全排列個數。

分析:令所有 ni 之和為 n,再設答案為 x。首先做全排列,然後把所有元素編號,其中第 s

種元素編號為 1~ns(比如,有三個 a,兩個 b,先排列成 aabba,然後可以編號為 a1a3b2b1a2)。

這樣做以後,由於編號後所有元素均不相同,方案總數為 n 的全排列數 n!。根據乘法原理,

我們得到了一個方程式: 1 2 3! ! ! ! !kn n n n x n= ,移項即可。

問題 5:可重複選擇的組合。有 n 個不同元素,每個元素可以選多次,一共選 k 個元素,有

多少種方法?比如 n=3,k=2 有 6 種:(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)。

分析:設第 i 個元素選 xi 個,問題轉化為求方程式 x1+x2+…+xn=k 的非負整數解的個數。令

yi=xi+1,則答案為 y1+y2+...+yn=k+n 的正整數解的個數。想像有 k+1 個數字“1”排成一排,

則問題等價於把這些“1”分成 n 個部分,有多少種方法?這相當於在 k+n-1 個“候選分隔

線”中選 n-1 個,即 C(k+n-1, n-1)=C(n+k-1, k)。 後一步用到了組合數的性質 2。

問題 6:單色三角形。給定空間裡的 n(n ≤ 1,000)個點,其中沒有三點共線。每兩個點之

間都用紅色或黑色線段連接。求 3 條邊同色的三角形個數。

分析:直接統計需要 O(n3)時間,需要優化。從反面考慮,只要求出了非單色三角形,就可

以間接得到單色三角形的個數。在每個非單色三角形中,恰好有兩個頂點連接兩條異色邊

(不包含不在此三角形中的邊),而且有一個公共點的兩條異色邊總是唯一對應一個非單色

三角形,因此如果第 i 個點連接了 ai 條紅邊 n-1-ai 條黑邊,則這些邊屬於 ai(n-1-ai) 個非單

色三角形。每個非單色三角形考慮了兩次,因此 終答案應除以 2,即

1

1( 1 )

2

n

i ii

a n a=

− −

》例題 1 象棋中的皇后(Chess Queen, UVa 11538)

在 2×2 棋盤上放兩個相互攻擊的皇后(一白一黑),一共有 12 種方法,如圖 2-1 所示。

Page 13: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

Chapter 02 數學基礎

- 2-5 -

圖 2-1

如果棋盤是 n×m 的,有多少種方法放置兩個相互攻擊的皇后?例如 n=100,m=223 時答案

為 10,907,100。

輸入格式

輸入由不超過 5,000 行組成,每行均為一組資料,包含兩個整數 n, m(0 ≤ n, m ≤ 106)。輸入結束旗標為 n=m=0。 輸出格式

對於每組資料,輸出在 n×m 棋盤上放兩個相互攻擊的皇后的方案數。輸出保證不超過 64 位元帶符號整數的範圍。

▼分析

因為只有兩個皇后,因此相互攻擊的方式只有兩個皇后在同一行、同一列或同一對角線 3

種情況。這 3 種情況沒有交集,因此可以用加法原理。設在同一行放兩個皇后的方案數為

A(n, m),同一列放兩個皇后的方案數為 B(n, m),同一對角線放兩個皇后的方案數為 D(n,

m),則答案為 A(n, m)+B(n, m)+D(n, m)。

A(n, m)的計算可以用乘法原理:放白後有 nm 種方法,放黑後有 m-1 種方法,相乘就是

nm(m-1)。也可以理解為先選一行(有 n 種方法),然後在這一行中選兩個位置做全排列,

因此有 m(m-1)種方案。根據乘法原理得到 nm(m-1)。

B(n, m)其實就等於 A(m, n)(想一想,為什麼),也就是 mn(n-1)。

求 D(n, m)的思考過程會稍微麻煩一些。不妨設 n ≤ m,所有/向的對角線,從左到右的長度

依次為

1

1,2,3, , 1, , , , , , 1, 2, ,2,1m n n

n n n n n n n− +

− − − 个

考慮到還有另一個方向的對角線,上面的整個結果還要乘以 2,即

Page 14: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

提升程式設計的解題思考力

- 2-6 -

1

1

( , ) 2 2 ( 1) ( 1) ( 1)n

i

D n m i i m n n n−

=

= − + − + −

其中1 1 1

2

1 1 1

( 1)(2 1) ( 1) ( 1)(2 4)( 1)

6 2 3

n n n

i i i

n n n n n n n ni i i i

− − −

= = =

− − − − −− = − = − = ,因此

( 1)(2 4) 2 ( 1)(3 1)( , ) 2 ( 1) ( 1)

3 3

n n n n n m nD n m m n n n

− − − − − = + − + − =

程式如下。

#include<iostream> //用 cin/cout。因為這樣可以與平臺無關的讀寫 64 位元整數,比較方便 #include<algorithm> //使用 swap using namespace std; int main() { unsigned long long n, m; // 大可以保存 264-1>1.8*1019 while(cin >> n >> m) { if(!n && !m) break; if(n > m) swap(n, m); //這樣就避免了對 n<=m 和 n>m 兩種情況分類討論 cout << n*m*(m+n-2)+2*n*(n-1)*(3*m-n-1)/3 << endl; } return 0; }

《提升程式設計的邏輯思考力》書中曾多次強調:計數問題一定要考慮算數運算溢出的問題。

我們用 64 位元不帶正負號的整數保存 n 和 m, 大可以保存 264-1 > 1.8×1019,則 nm(m+n-2) ≤

106×106×2×106 =2×1018,不會溢出;而 2n(n-1)(3m-n-1) ≤ 2×106×106×3×106=6×1018 也不會溢

出,不難驗證, 終答案也不會溢出註1。雖然本題的輸入保證答案不超過帶符號 64 位元

整數範圍內,但因為在運算結果中不會出現負數,使用無符號 64 位元整數更加保險。

》例題 2 數三角形(Triangle Counting, UVa 11401)

有多少種方法可以從 1, 2, 3, …, n 中選出 3 個不同的整數,使得以它們為三邊長可以組成

三角形?比如 n=5 時有 3 種方法,即 (2, 3, 4),(2, 3, 5),(3, 4, 5)。n=8 時有 22 種方法。

輸入格式

輸入包含多組測試資料,每組資料的第一行為整數 n(3≤ n ≤ 1,000,000)。輸入用 n < 3 的旗標結束。 輸出格式

對於每組資料,輸出其方案總數。

註1 注意,「除以 3」是在乘法之後,所以應當判斷除以 3 之前的部分是不是會溢出。

Page 15: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

Chapter 02 數學基礎

- 2-7 -

▼分析

三重循環的時間複雜度為 O(n3),採用這種方法肯定超時。這樣的規模即使是 O(n2) 時間的

演算法都很難承受,那麼只能進行一些數學分析了。

用加法原理,設 大邊長為 x 的三角形有 c(x) 個,另外兩條邊長分別為 y 和 z,根據三角

形不等式註2有 y+z > x。所以 z 的範圍是 x-y < z < x。

根據這個不等式,當 y=1 時 x-1 < z < x,顯然無解;y=2 時只有一個解(z=x-1);y=3 時有

兩個解(z=x-1 或者 z=x-2)……直到 y=x-1 時有 x-2 個解。根據等差數列求和公式,一共

有 0+1+2+…+(x-3)+(x-2)=(x-1)(x-2)/2 個解。

可惜,這並不是 c(x) 的正確數值,因為上面的解包含了 y=z 的情況,而且每個三角形算了

兩遍(想一想,為什麼)。解決方案很簡單,首先統計 y=z 的情況。y 的取值從 x/2+1 開始

到 x-1 為止,一共有 x-1-x/2=(x-1)/2 個解,然後把這部分解扣除,再除以 2,即

1 ( 1)( 2) 1( )

2 2 2

x x xc x

− − − = −

原題要求實際上是「 大邊長不超過 n 的三角形數目」f(n)。根據加法原理,f(n)=c(1)+c(2)

+…+c(n)。可以寫成遞推式 f(n)=f(n-1)+c(n)。程式碼如下。

#include<iostream> using namespace std; long long f[1000010]; //int 存不下 int main() { f[3] = 0; for(long long x = 4; x <= 1000000; x++) f[x] = f[x-1] + ((x-1)*(x-2)/2 - (x-1)/2)/2; //遞推 int n; while(cin >> n) { if(n < 3) break; cout << f[n] << endl; }

》例題 3 啦啦隊(Cheerleaders, UVa 11806)

在一個 m 行 n 列的矩形網格裡放 k 個相同的石子,問有多少種方法?每個格子 多放一個

石子,所有石子都要用完,並且第一行、 後一行、第一列、 後一列都得有石子。

註2 即兩邊之和大於第三邊。

Page 16: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

提升程式設計的解題思考力

- 2-8 -

輸入格式

輸入第一行為資料組數 T(T ≤ 50),每組資料包含 3 個整數 m, n, k(2 ≤ m, n ≤ 20,k ≤ 500)。 輸出格式

對於每組資料,輸出方案總數除以 1,000,007 的餘數。

▼分析

如果題目求的是「第一行、 後一行、第一列、 後一列都沒有石子」的方案數,該有多

好啊!這相當於一共只有 m-2 行 n-2 列,答案自然是 C((m-2)(n-2), k) 了。幸運的是,利

用容斥原理,我們可以把本題轉化為上述問題。

設滿足「第一行沒有石子」的方案集為 A, 後一行沒有石子的方案集為 B,第一列沒有

石子的方案集為 C, 後一列沒有石子的方案集為 D,全集為 S,則所求答案就是「在 S 中

但不在 A, B, C, D 任何一個集合中」的元素個數,可以用容斥原理求解。

在程式中,我們用二進位來表示 A, B, C, D 的所有「搭配」(S 對應於「空搭配」)。如果在

集合 A 和 B 中,相當於少了一行;如果在集合 C 或 D 中,相當於少了一列。假定 後剩

了 r 行 c 列,方法數就是 C(rc, k)。

#include<cstdio> #include<cstring> using namespace std; const int MOD = 1000007; const int MAXK = 500; int C[MAXK+10][MAXK+10]; int main() { memset(C, 0, sizeof(C)); C[0][0] = 1; for(int i = 0; i <= MAXK; i++) { C[i][0] = C[i][i] = 1; //千萬不要忘記寫邊界條件 for(int j = 1; j < i; j++) C[i][j] = (C[i-1][j] + C[i-1][j-1]) % MOD; } int T; scanf("%d", &T); for(int kase = 1; kase <= T; kase++) { int n, m, k, sum = 0; scanf("%d%d%d", &n, &m, &k); for(int S = 0; S < 16; S++) { //列舉所有 16 種“搭配方式” int b = 0, r = n, c = m; //b 用來統計集合的個數,r 和 c 是可以放置的行列數 if(S&1) { r--; b++; } //第一行沒有石頭,可以放石頭的行數 r 減 1 if(S&2) { r--; b++; } if(S&4) { c--; b++; } if(S&8) { c--; b++; }

Page 17: 導讀說明 - epaper.gotop.com.twepaper.gotop.com.tw/PDFSample/ACL038500.pdf · 如何閱讀本書 歡迎大家閱讀本書!為了最大限度地發揮本書的作用,強烈建議您在閱讀正文之前仔細地

Chapter 02 數學基礎

- 2-9 -

if(b&1) sum = (sum + MOD - C[r*c][k]) % MOD; //奇數個條件,做減法 else sum = (sum + C[r*c][k]) % MOD; //偶數個條件,做加法 } printf("Case %d: %d\n", kase, sum); } return 0; }