算法系列之七:愛因斯坦的思考題(下)
CheckGroupRelation()函式需要根據當前組group的位置進行適當的處理,如果當前組是第一個組或最後一個組,則group的相鄰組只有一個,就是最靠近group的組,其它情況下group的相鄰組都是兩個。CheckGroupRelation()函式的實現如下:
162 bool CheckGroupRelation(GROUP *groups, int groupIdx, ITEM_TYPE type, int value) 163 { 164 if(groupIdx == 0) 165 { 166 if(GetGroupItemValue(& 167 { 168 return false; 169 } 170 } 171 else if(groupIdx == (GROUPS_COUNT - 1)) 172 { 173 if(GetGroupItemValue(&groups[groupIdx - 1], type) != value) 174 { 175 return false; 176 } 177 } 178 else 179 { 180 if( (GetGroupItemValue(&groups[groupIdx - 1], type) != value) 181 && (GetGroupItemValue(&groups[groupIdx + 1], type) != value) ) 182 { 183 return false; 184 } 185 } 186 187 return 188 } |
最後是列舉演算法部分的說明。前面系列文章中多次使用窮舉法解決問題,但都是一維線性組合的列舉,本題則有些特殊,因為需要對不同型別的元素分別用窮舉法進行列舉,因此不是簡單的線性組合。本文演算法採用的列舉方法是對不同型別的元素分別用窮舉法進行列舉,然後再進行組合,這個組合不是線性關係的組合,而是類似階乘的幾何關係的組合。具體思路就是按照group中的元素順序,首先對房子根據顏色組合進行窮舉,每得到一組房子顏色組合後,就在此基礎上對住在房子裡的人的國籍進行窮舉,在房子顏色和國籍的組合結果基礎上,在對飲料型別進行窮舉,依次類推,直到窮舉完最後一種型別得到完整的窮舉組合。
現在來具體推算一下窮舉的過程,首先是對房子顏色進行窮舉。因為是5種顏色的不重複組合,因此應該有5!= 120個顏色組合結果,但是根據線索4“綠房子緊挨著白房子,在白房子的左邊”,相當於綠房子和白房子有穩定的繫結關係,實際就只有4!= 24個顏色組合結果。接下來對24個房子顏色組合結果中的每一個結果再進行住戶國籍的窮舉,理論上國籍也有5!= 120個結果,但是根據線索9“挪威人住在第一個房子裡面”,相當於固定第一個房子住得人始終是挪威人,因此就只有4!= 24個國籍組合結果。窮舉完房子顏色和國籍後就已經有24 x 24 = 576個組合結果了,接下來需要對這576個組合結果中的每一個結果再進行飲料型別的窮舉,理論上飲料型別也有5!= 120個結果,但是根據線索8“住在中間那個房子裡的人喝牛奶”,相當於固定了一個飲料型別,因此也只有4!= 24個飲料組合型別。窮舉完飲料型別後就得到了576 x 24 = 13824個組合結果,接下來對13824個組合結果中的每一個結果再進行寵物種類的窮舉,這一步沒有線索可用,共有5!= 120個結果。窮舉完寵物種類後就得到了13824 x 120 = 1658880個組合結果,最後對1658880個組合結果中的每一個結果再進行香菸品牌的窮舉,這一步依然沒有線索可用,共有5!= 120個結果。窮舉完香菸品牌後就得到了全部組合共1658880 x 120 = 199065600個結果,剩下的事情就是對這將近2億個結果進行判斷。
以下就是對房子顏色進行窮舉的演算法實現:
369 /* 遍歷房子顏色*/ 370 void EnumHouseColors(GROUP *groups, int groupIdx) 371 { 372 if(groupIdx == GROUPS_COUNT) /*遞迴終止條件*/ 373 { 374 ArrangeHouseNations(groups); 375 return; 376 } 377 378 for(int i = COLOR_BLUE; i <= COLOR_YELLOW; i++) 379 { 380 if(!IsGroupItemValueUsed(groups, groupIdx, type_house, i)) 381 { 382 groups[groupIdx].itemValue[type_house] = i; 383 if(i == COLOR_GREEN) //應用線索(4):綠房子緊挨著白房子,在白房子的左邊; 384 { 385 groups[++groupIdx].itemValue[type_house] = COLOR_WHITE; 386 } 387 388 EnumHouseColors(groups, groupIdx + 1); 393 } 394 } 395 } |
EnumHouseColors()函式每得到一個房子顏色的窮舉結果,就呼叫ArrangeHouseNations()函式進行進一步處理。ArrangeHouseNations() 函式做的事情就是繼續列舉住在每個房子裡的人的國籍,但是在這之前,先要根據已知條件進行預處理。比如已知規則(9)的描述:挪威人住在第一個房子裡,就可以將group[0]的國籍鎖定為NATION_NORWAY,從而減少一層遍歷。
361 void ArrangeHouseNations(GROUP *groups) 362 { 363 /*應用規則(9):挪威人住在第一個房子裡面;*/ 364 groups[0].itemValue[type_nation] = NATION_NORWAY; 365 EnumHouseNations(groups, 1); /*從第二個房子開始*/ 366 } |
實際上,真正的遍歷國籍過程是在EnumHouseNations()函式中完成。EnumHouseNations()函式每得到一個完整的房子與國籍關係,就呼叫ArrangePeopleDrinks()函式進一步遍歷每個人喝的飲料型別。
341 /*遍歷國家*/ 342 void EnumHouseNations(GROUP *groups, int groupIdx) 343 { 344 if(groupIdx == GROUPS_COUNT) /*遞迴終止條件*/ 345 { 346 ArrangePeopleDrinks(groups); 347 return; 348 } 349 350 for(int i = NATION_NORWAY; i <= NATION_GERMANY; i++) 351 { 352 if(!IsGroupItemValueUsed(groups, groupIdx, type_nation, i)) 353 { 354 groups[groupIdx].itemValue[type_nation] = i; 355 356 EnumHouseNations(groups, groupIdx + 1); 357 } 358 } 359 } 360 |
在每一組房子顏色和(住房客)國籍的對應關係上(根據前面的分析,將會有24 x 24 = 576組對應關係),ArrangePeopleDrinks()函式負責進一步排定它們和飲料型別的關係。ArrangePeopleDrinks()函式直接呼叫EnumPeopleDrinks()函式進行飲料型別的列舉,規則(8):“住在中間那個房子裡的人和牛奶”直接體現在EnumPeopleDrinks()函式中:
313 void EnumPeopleDrinks(GROUP *groups, int groupIdx) 314 { 315 if(groupIdx == GROUPS_COUNT) /*遞迴終止條件*/ 316 { 317 /*應用規則(8):住在中間那個房子裡的人喝牛奶;*/ 318 if(groups[2].itemValue[type_drink] == DRINK_MILK) 319 { 320 ArrangePeoplePet(groups); 321 } 322 return; 323 } 324 325 for(int i = DRINK_TEA; i <= DRINK_MILK; i++) 326 { 327 if(!IsGroupItemValueUsed(groups, groupIdx, type_drink, i)) 328 { 329 groups[groupIdx].itemValue[type_drink] = i; 330 EnumPeopleDrinks(groups, groupIdx + 1); 331 } 332 } 333 } |
在確定完飲料型別後(根據前面的分析,將會得到576 x 24 = 13824組結果),就要進一步遍歷每個人養的寵物(別忘了,魚也算寵物)。題目沒有對寵物提供更多的線索,因此ArrangePeoplePet()函式就是簡單的遍歷全部寵物組合,得到120種人和寵物的組合:
288 void EnumPeoplePats(GROUP *groups, int groupIdx) 289 { 290 if(groupIdx == GROUPS_COUNT) /*遞'b5?歸'b9?終'd6?止'd6?條'cc?件'bc?*/ 291 { 292 ArrangePeopleCigert(groups); 293 return; 294 } 295 296 for(int i = PET_HORSE; i <= PET_DOG; i++) 297 { 298 if(!IsGroupItemValueUsed(groups, groupIdx, type_pet, i)) 299 { 300 groups[groupIdx].itemValue[type_pet] = i; 301 302 EnumPeoplePats(groups, groupIdx + 1); 303 } 304 } 305 } |
人和寵物的關係也遍歷完成以後(根據前面的分析,將會得到13824 x 120 = 1658880組結果),就要最後確定每個人抽的香菸型別。關於人和香菸的關係,題目中也沒有給出更多的線索,因此ArrangePeopleCigert()函式僅僅是呼叫EnumPeopleCigerts()函式完成120種人和香菸的組合:
264 void EnumPeopleCigerts(GROUP *groups, int groupIdx) 265 { 266 if(groupIdx == GROUPS_COUNT) /*遞迴終止條件*/ 267 { 268 DoGroupsfinalCheck(groups); 269 return; 270 } 271 272 for(int i = CIGARET_BLENDS; i <= CIGARET_BLUEMASTER; i++) 273 { 274 if(!IsGroupItemValueUsed(groups, groupIdx, type_cigaret, i)) 275 { 276 groups[groupIdx].itemValue[type_cigaret] = i; 277 278 EnumPeopleCigerts(groups, groupIdx + 1); 279 } 280 } 281 } |
每當人和香菸的關係也遍歷完成後,就呼叫DoGroupsfinalCheck()對完整的組合進行檢查,檢查主要是基於Bind和Relation兩種型別的線索進行檢查(另一種型別的線索已經在遍歷的過程中體現)。根據本文前面給出的bind和Relation兩種型別的數學模型,DoGroupsfinalCheck()函式的實現非常簡單,就是逐個呼叫前面給出的CheckGroupRelation()函式和CheckGroupBind()函式對兩類關係逐個對比檢查,此處就不再列出程式碼。
圖(3)演示了在每一輪窮舉過程中正確結果組合出來的過程,在我的Intel P8600 CPU上,使用單核完成全部列舉和判斷共耗時26秒,得到符合全部線索的答案只有一個,就是本文開始給出的示例。
圖(3)每一輪窮舉過程中正確結果組合出來的過程
相關推薦
算法系列之七:愛因斯坦的思考題(下)
CheckGroupRelation()函式需要根據當前組group的位置進行適當的處理,如果當前組是第一個組或最後一個組,則group的相鄰組只有一個,就是最靠近group的組,其它情況下group的相鄰組都是兩個。CheckGroupRelation()函式的實現如下:
算法系列之七:愛因斯坦的思考題(上)
這是一個很有趣的邏輯推理題,傳說是愛因斯坦提出來的,他宣稱世界上只有2%的人能解出這個題目,傳說不一定屬實,但是這個推理題還是很有意思的。題目是這樣的,據說有五個不同顏色的房間排成一排,每個房間裡分別住著一個不同國籍的人,每個人都喝一種特定品牌的飲料,抽一種特定品牌的煙,養
[算法系列之二十]字典樹(Trie)
一 概述 又稱單詞查詢樹,Trie樹,是一種樹形結構,是一種雜湊樹的變種。典型應用是用於統計,排序和儲存大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。 二 優點 利用字串的公共字首來減少查詢時間,最大限度地減少無謂的字串比較,查詢效
白話經典算法系列之七 堆與堆排序
堆排序與快速排序,歸併排序一樣都是時間複雜度為O(N*logN)的幾種常見排序方法。學習堆排序前,先講解下什麼是資料結構中的二叉堆。二叉堆的定義二叉堆是完全二叉樹或者是近似完全二叉樹。二叉堆滿足二個特
簡單的程式詮釋C++ STL算法系列之十:search
C++STL的非變易演算法(Non-mutating algorithms)是一組不破壞操作資料的模板函式,用來對序列資料進行逐個處理、元素查詢、子序列搜尋、統計和匹配。 search演算法函式在一個序列中搜索與另一序列匹配的子序列。它有如下兩個原型
簡單的程式詮釋C++ STL算法系列之十三:copy
前面十二個演算法所展現的都屬於非變易演算法(Non-mutating algorithms)系列,現在我們來看看變易演算法。所謂變易演算法(Mutating algorithms)就是一組能夠修改容器元素資料的模板函式,可進行序列資料的複製,變換等。
算法系列之九:計算幾何與圖形學有關的幾種常用演算法(二)
3.6 用向量的叉積判斷直線段是否有交 向量叉積計算的另一個常用用途是直線段求交。求交演算法是計算機圖形學的核心演算法,也是體現速度和穩定性的重要標誌,高效並且穩定的求交演算法是任何一個CAD軟體都必需要重點關注的。求交包含兩層概念,一個是判斷是否相交,另一個是
Python極簡教程之七:資料格式化(format)
自 python 2.6 開始,新增了一種格式化字串的函式str.format(),可謂威力十足。那麼,他跟之前的%型格式化字串相比,有什麼優越的存在呢?讓我們來揭開它羞答答的面紗。 #語法 它通過{}和:來代替%。 位置 '{0},{1}'.format('kzc',18) # k
[算法系列之十]大資料量處理利器:布隆過濾器
【引言】 在日常生活中,包括在設計計算機軟體時,我們經常要判斷一個元素是否在一個集合中。比如在字處理軟體中,需要檢查一個英語單詞是否拼寫正確(也就是要判斷 它是否在已知的字典中);在 FBI,一個嫌疑人的名字是否已經在嫌疑名單上;在網路爬蟲裡,一個網址是否被訪問過等等。最直
算法系列之二十一:實驗資料與曲線擬合
12.1 曲線擬合12.1.1 曲線擬合的定義 曲線擬合(Curve Fitting)的數學定義是指用連續曲線近似地刻畫或比擬平面上一組離散點所表示的座標之間的函式關係,是一種用解析表示式逼近離散資料的方法。曲線擬合通俗的說法就是“拉曲線”,也就是將現有資料透過
算法系列之遞迴函式(七位數字)
七對數字 今有兩個1,兩個2,兩個3,...兩個7,把它們排成一行。 要求,兩個1間有1個其它數字,兩個2間有2個其它數字,以此類推,兩個7之間有7個其它數字。如下就是一個符合要求的排列: 17126425374635 當然,如果把它倒過來,也是符合要求的。 請你找出另一種符合要求的
算法系列之二十三:離散傅立葉變換之音訊播放與頻譜顯示
頻譜和均衡器,幾乎是媒體播放程式的必備物件,沒有這兩個功能的媒體播放程式會被認為不夠專業,現在主流的播放器都具備這兩個功能,foobar 2000的十八段均衡器就曾經讓很多人著迷。我用Winamp播放音樂(AOL已經在2013年12月20日停止了Winamp的
算法系列之十二:多邊形區域填充演算法--掃描線填充演算法(有序邊表法)
、掃描線演算法(Scan-Line Filling) 掃描線演算法適合對向量圖形進行區域填充,只需要直到多邊形區域的幾何位置,不需要指定種子點,適合計算機自動進行圖形處理的場合使用,比如電腦遊戲和三維CAD軟體的渲染等等。 對向量多邊形區域
算法系列之十五:迴圈和遞迴在演算法中的應用
一、遞迴和迴圈的關係1、 遞迴的定義順序執行、迴圈和跳轉是馮·諾依曼計算機體系中程式設計語言的三大基本控制結構,這三種控制結構構成了千姿百態的演算法,程式,乃至整個軟體世界。遞迴也算是一種程式控制結構,但是普遍被認為不是基本控制結構,因為遞迴結構在一般情況下都可以用精心設計的
算法系列之十二:多邊形區域填充演算法--遞迴種子填充演算法
平面區域填充演算法是計算機圖形學領域的一個很重要的演算法,區域填充即給出一個區域的邊界(也可以是沒有邊界,只是給出指定顏色),要求將邊界範圍內的所有象素單元都修改成指定的顏色(也可能是圖案填充)。區域填充中最常用的是多邊形填色,本文中我們就討論幾種多邊形區域
模擬退火算法系列之(一):通俗理解
為什麼我的眼裡常含淚水?因為我有一個演算法不會。為了節約點眼淚,今天我們就來介紹著名的模擬退火演算法,它是一種基於蒙特卡洛思想設計的近似求解最優化問題的方法。〇、一點歷史——如果你不感興趣,可以跳過美國
算法系列之十四:狼、羊、菜和農夫過河問題
題目描述:農夫需要把狼、羊、菜和自己運到河對岸去,只有農夫能夠划船,而且船比較小,除農夫之外每次只能運一種東西,還有一個棘手問題,就是如果沒有農夫看著,羊會偷吃菜,狼會吃羊。請考慮一種方法,讓農夫能夠安全地安排這些東西和他自己過河。 這個題目考察人的快速邏輯運算
算法系列之十二:多邊形區域填充演算法--掃描線種子填充演算法
1.3掃描線種子填充演算法 1.1和1.2節介紹的兩種種子填充演算法的優點是非常簡單,缺點是使用了遞迴演算法,這不但需要大量棧空間來儲存相鄰的點,而且效率不高。為了減少演算法中的遞迴呼叫,節省棧空間的使用,人們提出了很多改進演算法,其中一種就是掃描線種子填充演算
算法系列之十八:用天文方法計算二十四節氣(上)
二十四節氣在中國古代曆法中扮演著非常重要的角色,本文將介紹二十四節氣的基本知識,以及如何使用VSOP82/87行星執行理論計算二十四節氣發生的準確時間。 中國古代曆法都是以月亮執行規律為主,嚴格按照朔望月長度定義月,但是由於朔望月長度和地球迴歸年
算法系列之二十:計算中國農曆(一)
世界各國的日曆都是以天為最小單位,但是關於年和月的演算法卻各不相同,大致可以分為三類:陽曆--以天文年作為日曆的主要週期,例如:中國公曆(格里曆)陰曆--以天文月作為日曆的主要週期,例如:伊斯蘭曆陰陽曆--以天文年和天文月作為日曆的主要週期,例如:中國農曆我國