算法系列(1):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的取值,可以进一步推算其他字符代表的数字,上表中得到了四组目前合法的取值,但是并不是四组取值都能最终推算出正确的结果,本题的答案只有一个,也就是说只有一组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语言定义:

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}

};

2. 穷举算法

建立数学模型,其实就是为了让计算机理解题目并处理相关的数据,算法就是告诉计算机如何使用这些模型中的数据。本文介绍的是穷举算法,算法的核心其实前面已经提到了,就是穷举所有的字母和数字的组合,对每种组合进行合法性判断,如果是合法的组合,就输出结果。

整个算法的核心是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;

}

}

3. 结果验证

根据char_item和char_val的初始数据,求解本题的Google方程式:

SearchingResult(char_item, char_val, 0, OnCharListReady);

穷举算法可以得到两个结果(M和E可以互换):

777589 – 188103 = 589486

777589 – 188106 = 589483

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

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

注明来源:

来源:oRbIt

链接:http://blog.csdn.net/orbit/article/details/6529277

时间: 2024-10-09 20:05:38

算法系列(1):Google方程式的相关文章

高效的多维空间点索引算法 — Geohash 和 Google S2

原文地址:https://www.jianshu.com/p/7332dcb978b2 引子 每天我们晚上加班回家,可能都会用到滴滴或者共享单车.打开 app 会看到如下的界面: app 界面上会显示出自己附近一个范围内可用的出租车或者共享单车.假设地图上会显示以自己为圆心,5公里为半径,这个范围内的车.如何实现呢?最直观的想法就是去数据库里面查表,计算并查询车距离用户小于等于5公里的,筛选出来,把数据返回给客户端. 这种做法比较笨,一般也不会这么做.为什么呢?因为这种做法需要对整个表里面的每一

算法系列之二十三:离散傅立叶变换之音频播放与频谱显示

算法系列之二十三:离散傅立叶变换之音频播放与频谱显示 算法系列之二十三离散傅立叶变换之音频播放与频谱显示 导语 什么是频谱 1 频谱的原理 2 频谱的选择 3 频谱的计算 显示动态频谱 1 实现方法 2 杂项说明 结果展示 导语 频谱和均衡器,几乎是媒体播放程序的必备物件,没有这两个功能的媒体播放程序会被认为不够专业,现在主流的播放器都具备这两个功能,foobar 2000的十八段均衡器就曾经让很多人着迷.在上一篇对离散傅立叶变换介绍的基础上,本篇就进一步介绍一下频谱是怎么回事儿,下一篇继续介绍

[算法系列之二十]字典树(Trie)

一 概述 又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种.典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计. 二 优点 利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希表高. 三 性质 (1)根节点不包含字符,除根节点外每一个节点都只包含一个字符: (2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串: (3)每个节点的所有子节点包含的字符都不相同. 单词列表为"apps&

【白话经典算法系列之十七】 数组中只出现一次的数 其他三次

本文地址:http://blog.csdn.net/morewindows/article/details/12684497转载请标明出处,谢谢. 欢迎关注微博:http://weibo.com/MoreWindows 首先看看题目要求: 数组A中,除了某一个数字x之外,其他数字都出现了三次,而x出现了一次.请给出最快的方法找到x. 这个题目非常有意思,在本人博客中有<位操作基础篇之位操作全面总结>这篇文章介绍了使用位操作的异或来解决——数组中其他数字出现二次,而x出现一次,找出x.有<

三白话经典算法系列 Shell排序实现

山是包插入的精髓排序排序.这种方法,也被称为窄增量排序,因为DL.Shell至1959提出命名. 该方法的基本思想是:先将整个待排元素序列切割成若干个子序列(由相隔某个"增量"的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序. 由于直接插入排序在元素基本有序的情况下(接近最好情况),效率是非常高的,因此希尔排序在时间效率上比前两种方法有较大提高. 以n=10的一个数组49, 38, 65, 97

白话经典算法系列之四 直接选择排序及交换二个数据的正确实现

分类: 白话经典算法系列 2011-08-09 11:15 16682人阅读 评论(29) 收藏 举报 算法面试c 直接选择排序和直接插入排序类似,都将数据分为有序区和无序区,所不同的是直接播放排序是将无序区的第一个元素直接插入到有序区以形成一个更大的有序区,而直接选择排序是从无序区选一个最小的元素直接放到有序区的最后. 设数组为a[0…n-1]. 1.      初始时,数组全为无序区为a[0..n-1].令i=0 2.      在无序区a[i…n-1]中选取一个最小的元素,将其与a[i]交

算法系列笔记5(扩展数据结构-动态顺序统计和区间树)

在编程中,我们往往使用已有的数据结构无法解决问题,这是不必要急着创建新的数据结构,而是在已有数据结构的基础上添加新的字段.本节在上一次笔记红黑树这一基础数据结构上进行扩展,得出两个重要的应用-动态顺序统计和区间树. 动态顺序统计 在算法系列笔记2中我们在线性时间内完成了静态表的顺序统计,而这里我们在红黑树上进行扩展,在O(lgn)时间内完成该操作,主要包括返回第i 排名的元素os_select(i)和给定一个元素x,返回其排名(os_rank(x)). 思想:添加新项:在红黑树的结点上记录下该结

趣写算法系列之--匈牙利算法(真的很好理解)

[书本上的算法往往讲得非常复杂,我和我的朋友计划用一些简单通俗的例子来描述算法的流程] 匈牙利算法是由匈牙利数学家Edmonds于1965年提出,因而得名.匈牙利算法是基于Hall定理中充分性证明的思想,它是部图匹配最常见的算法,该算法的核心就是寻找增广路径,它是一种用增广路径求二分图最大匹配的算法. -------等等,看得头大?那么请看下面的版本: 通过数代人的努力,你终于赶上了剩男剩女的大潮,假设你是一位光荣的新世纪媒人,在你的手上有N个剩男,M个剩女,每个人都可能对多名异性有好感(-_-

白话经典算法系列之七 堆与堆排序

堆排序与高速排序,归并排序一样都是时间复杂度为O(N*logN)的几种常见排序方法.学习堆排序前,先解说下什么是数据结构中的二叉堆. 二叉堆的定义 二叉堆是全然二叉树或者是近似全然二叉树. 二叉堆满足二个特性: 1.父结点的键值总是大于或等于(小于或等于)不论什么一个子节点的键值. 2.每一个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆). 当父结点的键值总是大于或等于不论什么一个子节点的键值时为最大堆.当父结点的键值总是小于或等于不论什么一个子节点的键值时为最小堆.下图展示一个最小堆