算法系列之一 :Google方程式
算法系列之一 : Google方程式
有一個字元組成的等式:WWWDOT - GOOGLE = DOTCOM,每個字元代表一個0-9之間的數字,WWWDOT、GOOGLE和DOTCOM都是合法的數字,不能以0開頭。請找出一組字元和數字的對應關係,使它們互相替換,並且替換後的數字能夠滿足等式。這個字元等式是Google公司能力傾向測試實驗室的一道題目,這種題目主要考察人的邏輯推導能力和短期記憶能力,通常棋下的好的人解決這類問題會更得心應手一些(飛行棋例外)。此型別的題目有很多變種,各種程式設計比賽中常常能見到它們的身影。比如2005年的GOOGLE中國程式設計挑戰賽第二輪淘汰賽有一道名為“SecretSum”的500分的競賽題,與本題如出一轍,只不過字母都是三個,而且用的是加法計算。現在言歸正傳,先看看如何分析這個問題。
以人的思維方式分析問題
將橫式改成豎式可能更直觀一些:
根據以上豎式減法,從左向右依次可以得到6個算式,分別是:
W – G = D (算式 1)
W – O = O (算式 2)
W – O = T (算式 3)
D – G = C (算式 4)
O – L = O (算式 5)
T – E = M (算式 6)
根據以上6個算式可以分析出兩個關鍵資訊:一個是W要足夠大,因為考慮到它可能被借位的情況還要等於G和D的和;另一個則是本問題的突破口,就是算式2和算式3兩次出現的W – O計算。現在分析算式2和算式3,根據是否需要借位,算式2和算式3一共有四種借位組合結果,下面分別對這四種借位組合結果進行分析。
1. W – O = T不需要借位,W – O = O也不需要借位
由於W – O = T和W – O = O都不需要借位,則可由算式2變形得到算式1.1:
W = 2O (算式1.1)
將算式1.1帶入算式3,又可以得到算式1.2:
O = T (算式 1.2)
根據算式1.2,O和T代表的數字是同一個數字,這與題目要求不符,因此,這種借位組合不能得到正確的結果。
2. W – O = T需要借位,W – O = O不需要借位
根據借位情況,對算式2和算式3進行借位修正,得到兩個修正算式:
W – 1 – O = O (算式2.1)
W + 10 – O = T (算式2.2)
由算式2.1變形得到算式2.3:
W = 2O + 1 (算式2.3)
將算式2.3帶入算式2.2可以得到算式2.4:
T = O + 11 (算式2.4)
對算式2.3分析,由於W是個位數,最大值是9,所以O的取值只能是1-4,但是無論如何,由算式2.4計算出的T都超過9,這與題目要求不符,因此,這種情況也是無解的情況。
3. W – O = T不需要借位,W – O = O需要借位
根據借位情況,對算式2和算式3進行借位修正,得到兩個修正算式:
W + 10 – O = O (算式3.1)
W – O = T (算式3.2)
由算式3.1變形得到算式3.3:
W = 2O - 10 (算式3.3)
將算式3.3帶入算式3.2由可得到算式3.4:
O – 10 = T (算式3.4)
O顯然是不能比10大的個位數,因此,這種情況也是無解的情況
4. W – O = T需要借位,W – O = O也需要借位
根據借位情況,對算式2和算式3進行借位修正,得到兩個修正算式:
W – 1 + 10 – O = O (算式4.1)
W + 10 – O = T (算式4.2)
由算式4.1變形得到算式4.3:
W = 2O - 9 (算式4.3)
將算式4.3代入算式4.2得到算式4.4:
O + 1 = T (算式4.4)
由於W不能小於0,因此,根據算式4.3,O的取值最小為5。根據算式4.4繼續分析,因為T不能大於9,因此O的最大值只能取值為8。根據O的取值區間[5,8],可依次計算出W和T的值,如下表所示:
O | W | T |
5 | 1 | 6 |
6 | 3 | 7 |
7 | 5 | 8 |
8 | 7 | 9 |
已知O、W、T的取值,可以進一步推算其他字元代表的數字,上表中得到了四組目前合法的取值,但是並不是四組取值都能最終推算出正確的結果,本題的答案只有一個,也就是說只有一組O、W、T的取值是正確的,下面就分別進行分析。
O = 5, W = 1, T = 7
在這種情況下,考察算式1:W – G = D,W = 1顯然無法滿足此種情況,更何況算式2: W – O = O還要從它這裡借位,因此,這種情況無解。
O = 6, W = 3, T = 7
在這種情況下,算式2: W – O = O還要從它這裡借位,因此算式1:W – G = D對應的實際情況是2 – G = D,G和D不能同時為1,而且G和D都是第一位數字,不能是0,因此無法滿足算式1,這種情況也是無解。
O = 7, W = 5, T = 8
在這種情況下,需要考察另另外兩個關鍵算式,分別是算式5和算式6。根據這兩個算式是否需要借位進行不同的假設,根據組合,仍然有四種假設,下面分別分析這四種假設:
假設一:算式5需要借位,算式6不需要借位。則此時算式5可修正為O + 10 – L = O,推算出L = 10,顯然不符合題意,假設一不成立;
假設二:算式5需要借位,算式6需要借位,則算式5和算式6應該修正為算式4.3.1和算式4.3.2:
O -1 + 10 – L = O (算式4.3.1)
T + 10 – E = M (算式4.3.2)
因為已知T=8,帶入4.3.2可得E+M=18,顯然對於兩個不相同的個位數無法滿足這個等式,因此假設二也不成立;
假設三:算式5不需要借位,算式6不需要借位,此時根據算式6可知E和M的和是8(T=8),排除E=M=4的情況後,E和M的組合可以是(1,7)、(2,6)和(3,5),又因為數字5和7分別被W和O使用,因此E和M只能是2或6。再回頭來看算式1,因為算式2需要借位,算式1實際相當於G + D = 4,G和D只能取值1和3,若G=1,D=3,則根據算式4計算出C=2,這與E或M矛盾。若G=3,D=1,則算式4需要借位,這又與算式3的假設矛盾。由此看來,假設三也不能得到正確的結果;
假設四:算式5不需要借位,算式6需要借位,此時根據算式5被修正為O – 1 – L = O,這種情況下也是無解的。
O = 8, W = 7, T = 9
在這種情況下,根據算式5和算式6是否借位的假設,仍然有四種假設,下面分別分析這四種假設:
假設一:算式5需要借位,算式6不需要借位。則此時算式5可修正為O + 10 – L = O,推算出L = 10,顯然不符合題意,假設一不成立;
假設二:算式5需要借位,算式6需要借位,則算式5和算式6應該修正為算式4.4.1和算式4.4.2:
O -1 + 10 – L = O (算式4.4.1)
T + 10 – E = M (算式4.4.2)
因為已知T=9,帶入4.4.2可得E+M=19,兩個不同的個位數的和不可能大於18,因此假設二也不成立;
假設三:算式5不需要借位,算式6不需要借位,此時根據算式6可知E和M的和是9(T=9),E和M的組合可以是(1,8)、(2,7) 、(4,5)和(3,6),又因為數字8和7分別被O和W使用,因此E和M只能是(4,5)和(3,6)。進一步假設E=4,M=5(反過來E=5,M=4是一樣的,不影響分析)。再看算式1,因為算式2需要借位,算式1實際相當於G + D = 6,由於M或E是5,所以G和D只能取值2和4。若G=2,D=4,則根據算式4計算出C=2,這與G=2矛盾。若G=4,D=2,則算式4需要借位,這又與算式3的假設矛盾,因此E=4,M=5的情況無解。再次進一步假設E=3,M=6(反過來E=6,M=3是一樣的,不影響分析)。同樣再看算式1,G和D的值可取是(2,4)和(1,5),G和D取值1和2的情況剛剛分析過無解,因此G和D的取值只能是1和5,前面分析過,算式4沒有借位,也就是說要保證D > G,因此,D=5,G=1,根據算式4計算出C=4,這樣就得到了一組解:O = 8,W = 7,T = 9,D = 5,L = 0, G = 1,C = 4,E = 3/6,M = 6/3。最終的等式是:
777589 - 188103 = 589486
或
777589 - 188106 = 589483
假設四:算式5不需要借位,算式6需要借位,此時根據算式5被修正為O – 1 – L = O,這種情況下也是無解的。
完整的分析過程結束,得到了一組答案,事實上通過計算機窮舉演算法也只能得到這一組結果,下面就看看如何用計算機演算法求解本題的答案。
用計算機窮舉所有的解
以上是用人的思維方式的解題過程,如果方法正確,加上運氣好(三次假設都是正確的,避免在錯誤分支上浪費時間),兩分鐘內就可得到結果。但是考慮到更通用的情況,字母數字沒有規律,也沒有可供分析的入手點和線索,比如:
AAB – BBC = CCD
這樣的問題,該什麼方法解決呢?只能“猜想”,用窮舉的方法試探每一種猜想,對每個字母和數字窮舉所有可能的組合,直到得到正確的結果。當然,這樣的力氣活交給計算機做是最合適不過了。
1. 建立數學模型
要想讓計算機解決問題,就要讓計算機能夠理解題目,這就需要建立一個計算機能夠識別、處理的數學模型,首先要解決的問題就是建立字母和數字的對映關係的數學模型。本題的數學模型很簡單,就是一個字母二元組:{char, number}。考察等式:
WWWDOT - GOOGLE = DOTCOM
共出現了9個不同的字母:W、D、O、T、G、L、E、C和M,因此,最終的解應該是9個字母對應的字母二元組向量:[ {'W', 7}, {'D', 5}, {'O', 8}, {'T', 9}, {'G', 1}, {'L', 0}, {'E', 3}, {'C', 4}, {'M', 6} ]。窮舉演算法就是對這個字母二元組向量中每個字母二元組的number元素進行窮舉,number的窮舉範圍就是0-9共10個數字,當然,根據題目要求,有一些字元不能為0,比如W、G和D。排列組合問題的窮舉多使用多重迴圈,看樣子這個窮舉演算法應該是9重迴圈了,在每層迴圈中對一個字母進行從0到9遍歷。問題是,必須這樣嗎,對於更通用的情況,不是9個字母的問題怎麼辦?首先思考一下是否每次都要遍歷0-9。題目要求每個字母代表一個數字,而且不重複,很顯然,對每個字母進行的並不是排列,而是某種形式的組合,舉個例子,就是如果W字母佔用了數字7,那麼其它字母就肯定不是7,所以對D字母遍歷是就可以跳過7。進一步,假設某次遍歷的字母二元組向量中除M字母外其它8個字母已經有對應的數字了,比如:
[ {'W', 7}, {'D', 5}, {'O', 8}, {'T', 9}, {'G', 1}, {'L', 0}, {'E', 3}, {'C', 4}, {'M', ?} ] (序列-1)
那麼M的可選範圍就只有2和6,顯然沒必要使用9重迴圈。
現在換一種想法,對9個二元組的向量進行遍歷,可以分解為兩個步驟,首先確定第一個二元組的值,然後對剩下的8個二元組進行遍歷。顯然這是一種遞迴的思想(分治),演算法很簡單,但是要對10個數字的使用情況進行標識,對剩下的二元組進行遍歷時只使用沒有佔用標識的數字。因此還需要一個標識數字佔用情況的數字二元組定義,這個二元組可以這樣定義:{number, using},0-9共有10個數字,因此需要維護一個長度為10的數字二元組向量。數字二元組向量的初始值是:
[{0, false}, {1, false},{2, false},{3, false},{4, false},{5, false},{6, false},{7, false},{8, false},{9, false}] (序列-2)
每進行一重遞迴就有一個數字的using標誌被置為true,當字母二元組向量得到(序列-1)的結果時,對應的數字二元組向量的值應該是:
[{0, true}, {1, true},{2, false},{3, true},{4, true},{5, true},{6, false},{7, true},{8, true},{9, true}] (序列-3)
此時遍歷這個數字二元組向量就可以知道M字母的可選值只能是2或6。
窮舉遍歷的結束條件是每層遞迴中遍歷完所有using標誌是false的數字,最外一層遍歷完所有using標誌是false的數字就結束了演算法。
根據題目要求,開始位置的數字不能是0,也就是W、G和D這三個字母不能是0,這是一個“剪枝”條件,要利用起來,因此,對字母二元組進行擴充成字母三元組,新增一個leading標誌:{char, number, leading}。下面就是這個數學模型的C語言定義:
4 typedef struct 5 { 6 char c; 7 int value; 8 bool leading; 9 }CharItem; 10 11 typedef struct 12 { 13 bool used; 14 int value; 15 }CharValue; |
根據此數學模型初始化字母三元組和數字二元組向量:
29 30 CharItem char_item[max_char_count] = 31 { 32 { 'W', -1, true }, { 'D', -1, true }, { 'O', -1, false }, 33 { 'T', -1, false }, { 'G', -1, true }, { 'L', -1, false }, 34 { 'E', -1, false }, { 'C', -1, false }, { 'M', -1, false } 35 }; 36 37 CharValue char_val[max_number_count] = 38 { 39 {false, 0}, {false, 1}, {false, 2}, {false, 3}, 40 {false, 4}, {false, 5}, {false, 6}, {false, 7}, 41 {false, 8}, {false, 9} 42 }; 43 |
2. 窮舉演算法
建立數學模型,其實就是為了讓計算機理解題目並處理相關的資料,演算法就是告訴計算機如何使用這些模型中的資料。本文介紹的是窮舉演算法,演算法的核心其實前面已經提到了,就是窮舉所有的字母和數字的組合,對每種組合進行合法性判斷,如果是合法的組合,就輸出結果。
整個演算法的核心是SearchingResult()函式,其實這個函式非常簡單:
54 55 void SearchingResult(CharItem ci[max_char_count], 56 CharValue cv[max_number_count], 57 int index, CharListReadyFuncPtr callback) 58 { 59 if(index == max_char_count) 60 { 61 callback(ci); 62 return; 63 } 64 65 for(int i = 0; i < max_number_count; ++i) 66 { 67 if(IsValueValid(ci[index], cv[i])) 68 { 69 cv[i].used = true;/*set used sign*/ 70 ci[index].value = cv[i].value; 71 SearchingResult(ci, cv, index + 1, callback); 72 cv[i].used = false;/*clear used sign*/ 73 } 74 } 75 } 76 |
SearchingResult()函式有四個引數,ci就是儲存遍歷結果的字母三元組向量,cv是儲存遍歷過程中數字佔用情況的數字二元組向量,index是當前處理的字母三元組在字母三元組向量中的位置索引,0表示第一個字母三元組。callback是一個回撥函式,當ci中所有三元組都分配了數字,就呼叫callback對這組解進行判斷,如果滿足算式就輸出結果。SearchingResult()函式的程式碼分兩部分,前一部分是結束條件判斷和結果輸出,後一部分是演算法的關鍵。演算法就是遍歷cv中的所有數字二元組,對於每一個可用的數字(當前沒有被佔用,並且滿足第一個數字不是0的要求),首先設定佔用標誌,然後將當前字母三元組的值與這個數字的值繫結,最後遞迴處理下一個字母三元組。
SearchingResult()函式是一個通用過程,負責字母和數字的組合,回撥函式(callback)負責根據題目要求對SearchingResult()函式得到的字母和數字的組合進行篩選,只輸出正確的組合。對於本題,回撥函式可以這樣實現:
6 7 void OnCharListReady(CharItem ci[max_char_count]) 8 { 9 char *minuend = "WWWDOT"; 10 char *subtrahend = "GOOGLE"; 11 char *diff = "DOTCOM"; 12 13 int m = MakeIntegerValue(ci, minuend); 14 int s = MakeIntegerValue(ci, subtrahend); 15 int d = MakeIntegerValue(ci, diff); 16 if((m - s) == d) 17 { 18 std::cout << m << " - " << s << " = " << d << std::endl; 19 } 20 } 21 |
3. 結果驗證
根據char_item和char_val的初始資料,求解本題的Google方程式:
SearchingResult(char_item, char_val, 0, OnCharListReady);
窮舉演算法可以得到兩個結果(M和E可以互換):
777589 - 188103 = 589486
777589 - 188106 = 589483
由於演算法具有通用性,對於前文例子中的等式:
AAB – BBC = CCD
只需要構造新的字母三元組向量,並修改回撥函式的過濾資料即可。新的字母三元組可按照如下方式構造:
33 34 CharItem char_item[max_char_count] = { {'A', -1, true}, {'B', -1, true}, {'C', -1, true}, 35 {'D', -1, false} }; 36 |
回撥函式與前文的OnCharListReady()函式類似,此處不再列出。根據新的字元三元組和回撥函式執行演算法,可以得到13組結果:
443 - 331 = 112
553 - 332 = 221
554 - 441 = 113
665 - 551 = 114
774 - 443 = 331
775 - 552 = 223
776 - 661 = 115
885 - 553 = 332
886 - 662 = 224
887 - 771 = 116
995 - 554 = 441
997 - 772 = 225
998 - 881 = 117
對於加法、乘法和除法算式,同樣只要使用不用的回撥函式進行結果判斷即可,不需要修改SearchingResult()函式,例如加法算式:
ABC + ABC = BCE
可以得到5組結果:
124 + 124 = 248
125 + 125 = 250
249 + 249 = 498
374 + 374 = 748
375 + 375 = 750