1. 程式人生 > >算法系列之一 :Google方程式

算法系列之一 :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