算法系列 - Google方程式

算法系列 Google方程式

 

有一个字符组成的等式:WWWDOT - GOOGLE = DOTCOM,每个字符代表一个09之间的数字,WWWDOTGOOGLEDOTCOM都是合法的数字,不能以0开头。请找出一组字符和数字的对应关系,使得替换后的数字能够满足等式。此类型的题目有很多,这个显然和Google有关系,据说这是一道Google公司的面试题。这种题目主要考察人的逻辑推导能力和短期记忆能力,通常棋下的好的人解决这类问题会更得心应手一些(飞行棋例外),因为他们通常都是走一个想好几步,对于解决这种每次面临很多种情况,每种情况又会派生出很多不同的子情况问题非常顺手。首先用人的思维习惯解决这个问题:

 

将横式改成竖式可能更直观一些:

首先可以得到一个信息,一个是W要足够大,因为考虑到它可能被借位的情况还要满足GD的和,但是问题的突破口应该是两次出现的W – O,一次是:

 

W – O = T               1

 

另一次是:              2

 

W – O = O

 

现在分析一下可能出现的情况:

 

第一种情况,W – O = T不需要借位,W – O = O不需要借位,则可得出两个修正等式:

 

W – O = T              1.1

W – O = O              1.2

 

由等式(1.2)变形得到(1.2.1):

 

W = 2O                1.2.1

将(1.2.1)代入等式(1.1)得到等式(1.1.1

 

O = T   1.1.1

 

这显然与题目要求不符,因此这种情况无解。

 

第二种情况,W – O = T需要借位,W – O = O不需要借位,则可得出两个修正等式:

 

W + 10 – O = T            2.1

W – 1 – O = O             2.2

 

由等式(2.2)变形得到(2.2.1):

 

W – 1  = 2O   2.2.1

 

由于W是个个位数,所以O的取值只能是15,但是分析O15的值得到的WO组合都不能满足等式(2.1),所以这种情况也是无解的情况。

 

第三种情况,W – O = T不需要借位,W – O = O需要借位,则可得出两个修正等式:

 

W – O = T               3.1

W + 10 – O = O           3.2

 

由等式(3.2)变形得到(3.2.1):

 

W = 2O - 10             3.2.1

 

将(3.2.1)代入等式(3.1)得到等式(3.1.1

 

O – 10 = T              3.1.1

 

O显然是不能比10大的个位数,因此这种情况也是无解的情况。

 

第四种情况,W – O = T需要借位,W – O = O也需要借位,则可得出两个修正等式:

 

W + 10 – O = T            4.1

W – 1 + 10 – O = O         4.2

 

由等式(4.2)变形得到(4.2.1):

 

W = 2O - 9               4.2.1

 

将(4.2.1)代入等式(4.1)得到等式(4.1.1

 

O + 1 = T                4.1.1

 

W必须是整数且不能是0,也不能是12这样的小值,当O9时,T会大于9,不满足题目要求,因此O的取值只能是78,现在假设:

   AO = 7,则W = 5T = 8

考察等式:

O – L = O 4.a.1),进一步假设:

A1)此时(4.a.1)需要借位,则等式(4.a.1)演变为:

O + 10 – L = O,此时L10不满足题目要求,无解。

A2)再假设T – E = M需要借位,则等式(4.a.1)演变为:

O -1 – L = O,此时L = 1,并且得到等式:

T + 10 – E = M 4.a.2

T=8代入,得到E + M = 18,只有E=M=9才能满足等式,但是和题目要求不符,无解。

A3)再假设T – E = M不需要借位,则L = 0EM分别为35,或53。此时得到一个中间结果,如下所示:

     

          由于此假设是建立在W – O = O需要借位的基础上的,因此第一个竖式实际上应该是W – 1 – G = D,将W=5代入得到(4.a.3)

          G + D = 4  (4.a.3)

          此时GD只能是13,无论如何都和ME冲突,因此无解。

   

     因此,当O = 7时无法得到有效解,只能继续假设O = 8

BO = 8,则W = 7T = 9

考察等式:

O – L = O 4.b.1),进一步假设:

B1)此时(4.b.1)需要借位,则等式(4.b.1)演变为:

O + 10 – L = O,此时L10不满足题目要求,无解。

B2)再假设T – E = M需要借位,则等式(4.b.1)演变为:

O -1 – L = O,此时L = 1,并且得到等式:

T + 10 – E = M 4.b.2

T=9代入,得到E + M = 19,两个不同的个位数的和不可能大于18,因此无解。

B3)再假设T – E = M不需要借位,则L = 0EM分别为36,或63。此时得到一个中间结果,如下所示:

     

          由于此假设是建立在W – O = O需要借位的基础上的,因此第一个竖式实际上应该是W – 1 – G = D,将W=7代入得到(4.b.3)

          G + D = 6  (4.b.3)

          此时GD只能是1524,由于D – G = C没有发生借位,可知D > G。因此,D可为45G12,假设D = 4G = 2,得到C = 2,与题目要求不符,因此只能是D = 5 G = 1C = 4,所以可得到一个解:O = 8W = 7T = 9D = 5L = 0 G = 1C = 4E = 3/6M = 6/3。最终的等式是:

          777589 - 188103 = 589486

         

777589 - 188106 = 589483

 

以上是用人的思维方式的解题过程,如果方法正确,加上运气好(三次假设都是正确的,避免在错误分支上浪费时间),两分钟内就可得到结果。但是考虑到更通用的情况,字母数字没有规律,也没有可供分析的入手点和线索,比如:

AAB – BBC = CCD

这样的问题,该什么方法解决呢?只能“猜想”,用穷举的方法试探每一种猜想,对每个字母和数字穷举所有可能的组合,直到得到正确的结果。当然,这样的力气活交给计算机做是最合适不过了。

要想让计算机解决问题,就要让计算机能够理解题目,这就需要建立一个计算机能够识别、处理的数学模型,首先要解决的问题就是建立字母和数字的映射关系的数学模型。本题的数学模型很简单,就是一个字母二元组:{char, number}。考察等式:

 

WWWDOT - GOOGLE = DOTCOM

 

共出现了9个不同的字母:WDOTGLECM,因此,最终的解应该是9个字母对应的字母二元组向量:[ {'W', 7}, {'D', 5}, {'O', 8}, {'T', 9}, {'G', 1}, {'L', 0}, {'E', 3}, {'C', 4}, {'M', 6} ]。穷举算法就是对这个字母二元组向量中每个字母二元组的number元素进行穷举,number的穷举范围就是0910个数字,当然,根据题目要求,有一些字符不能为0,比如WGD。排列组合问题的穷举多使用多重循环,看样子这个穷举算法应该是9重循环了,在每层循环中对一个字母进行从09遍历。问题是,必须这样吗,对于更通用的情况,不是9个字母的问题怎么办?首先思考一下是否每次都要遍历09。题目要求每个字母代表一个数字,而且不重复,很显然,对每个字母进行的并不是排列,而是某种形式的组合,举个例子,就是如果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的可选范围就只有26,显然没必要使用9重循环。

 

现在换一种想法,对9个二元组的向量进行遍历,可以分解为两个步骤,首先确定第一个二元组的值,然后对剩下的8个二元组进行遍历。显然这是一种递归的思想(分治),算法很简单,但是要对10个数字的使用情况进行标识,对剩下的二元组进行遍历时只使用没有占用标识的数字。因此还需要一个标识数字占用情况的数字二元组定义,这个二元组可以这样定义:{number, using}09共有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字母的可选值只能是26

 

穷举遍历的结束条件是每层递归中遍历完所有using标志是false的数字,最外一层遍历完所有using标志是false的数字就结束了算法。

 

根据题目要求,开始位置的数字不能是0,也就是WGD这三个字母不能是0,这是一个“剪枝”条件,要利用起来,因此,对字母二元组进行扩充成字母三元组,添加一个leading标志:{char, number, leading}。现在算法的整体实现都讨论了,剩下的就是用代码实现这个算法了。

 

首先是用数据结构描述字母三元组和数字二元组:

 

typedef struct

{

    char c;

    int value;

    bool leading;

}CharItem;

 

typedef struct

{

    bool used;

    int value;

}CharValue;

 

算法首先初始化字三元组和数字二元组向量

CharItem char_item[max_char_count] = {

{ 'W', -1, true  }, { 'D', -1, true  }, { 'O', -1, false },

      { 'T', -1, false }, { 'G', -1, true  }, { 'L', -1, false },

      { 'E', -1, false }, { 'C', -1, false }, { 'M', -1, false }

};

 

CharValue char_val[max_number_count] = {

{false, 0}, {false, 1}, {false, 2}, {false, 3},

      {false, 4}, {false, 5}, {false, 6}, {false, 7},

      {false, 8}, {false, 9}

};

 

然后调用SearchingResult()函数开始递归遍历

 

SearchingResult(char_item, char_val, 0, OnCharListReady);

 

整个算法的核心是SearchingResult()函数,其实这个函数非常简单:

void SearchingResult(CharItem ci[max_char_count],

CharValue cv[max_number_count],

int index, CharListReadyFuncPtr callback)

{

    if(index == max_char_count)

    {

        callback(ci);

        return;

    }

 

    for(int i = 0; i < max_number_count; ++i)

    {

        if(IsValueValid(ci[index], cv[i]))

        {

            cv[i].used = true;/*set used sign*/

            ci[index].value = cv[i].value;

            SearchingResult(ci, cv, index + 1, callback);

            cv[i].used = false;/*clear used sign*/

        }

    }

}

 

SearchingResult()函数有四个参数,ci就是存储遍历结果的字母三元组向量,cv是存储遍历过程中数字占用情况的数字二元组向量,index是当前处理的字母三元组在字母三元组向量中的位置索引,0表示第一个字母三元组。callback是一个回调函数,当ci中所有三元组都分配了数字,就调用callback对这组解进行判断,如果满足算式就输出结果。SearchingResult()函数的代码分两部分,前一部分是结束条件判断和结果输出,后一部分是算法的关键。算法就是遍历cv中的所有数字二元组,对于每一个可用的数字(当前没有被占用,并且满足第一个数字不是0的要求),首先设置占用标志,然后将当前字母三元组的值与这个数字的值绑定,最后递归处理下一个字母三元组

SearchingResult()函数是一个通用过程,负责字母和数字的组合,回调函数(callback)负责根据题目要求对SearchingResult()函数得到的字母和数字的组合进行筛选,只输出正确的组合。对于本题,回调函数可以这样实现:

 

void OnCharListReady(CharItem ci[max_char_count])

{

    char *minuend    = "WWWDOT";

    char *subtrahend = "GOOGLE";

    char *diff       = "DOTCOM";

 

    int m = MakeIntegerValue(ci, minuend);

    int s = MakeIntegerValue(ci, subtrahend);

    int d = MakeIntegerValue(ci, diff);

    if((m - s) == d)

    {

        std::cout << m << " - " << s << " = " << d << std::endl;

    }

}

运行算法可以得到两个结果

 

777589 - 188103 = 589486

777589 - 188106 = 589483

ME可以互换。

 

由于算法具有通用性,对于前文例子中的等式:

 

AAB – BBC = CCD

 

只需要构造新的字母三元组向量,并修改回调函数的过滤数据即可。新的字母三元组可按照如下方式构造:

 

CharItem char_item[max_char_count] = {

{'A', -1, true}, {'B', -1, true}, {'C', -1, true}, {'D', -1, false}

};

 

 

 

回调函数与前文的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

 

完整的实现代码请点击这里查看

 

posted @ 2011-06-07 12:04  oRbIt  阅读(610)  评论(0编辑  收藏  举报