解决问题的技巧

2020-11-09 02:00:02

第一类包括学习语言语法、结构和模式等内容。我将其概括为能够连接和利用行业中的无数工具--语言、框架、API、库--来创建软件。通常有关于这些事情的教程。

第二类包括一些比较难理解的东西,但最好的描述可能是解决问题的方法。它是分析、故障排除、调试或解决问题的能力。它是一种用抽象的想法进行推理并将其转化为代码的能力。

这两个类别之间肯定有一些重叠,但我喜欢这样想。

很难具体说明怎样才能擅长解决问题。我相信这是因为我们对周围世界的看法略有不同。每个人都学到了不同的东西。然而,至少有一些技巧可以让我向你学习,反之亦然。

像我将要分享的这样的问题可以成为学习解决问题的很好的工具。这是来自Project Euler的。我的意图不是简单地破坏答案。我想分享我弄清楚这个问题的过程,希望能够找出一些可以用来解决各种问题的具体策略。

您可以在这里找到原始的问题陈述。它很短,所以花点时间通读一下。

挑战在于确定最短的可能密码是什么。以下是键盘记录程序文件的摘录。

我开始用铅笔和纸坐下来,从清单上写下一些数字。写作帮助我开始思考这个问题。

我想着清单上的这些数字能告诉我关于这个秘密号码的什么信息。或许我能找到一种方法至少算出它的长度。列表中大约有50个数字,所以这个数字可能与秘密数字有某种关联。

这些都是我脑海中浮现的几个问题。如果你觉得答案非常明显,那么恭喜你,你可能比我聪明。在第一步中,最重要的是你要问问题。

我很快意识到不是的,我得到的名单长度并不能告诉我太多,但你总得从某个地方开始吧。接下来,我的想法转向这样一个事实:每个数字都有三位数长。

如果给我一份两位数的数字列表,或者甚至只给我一位数字的列表,会怎么样?关于密码的长度,这能告诉我些什么呢?三位数有什么特别之处吗?这些都是有价值的问题,因为它帮助我简化了问题。

我列了一张一位数的单子,试着想一想,我如何才能用它来解决同样的问题。

给出一个这样的列表作为线索,我可以肯定地说,这个秘密数字至少有6、2、1和0,但我也泄露了关于这个问题的基本信息:数字出现的顺序。一位数字的列表太含糊了。密码可能是6210、2601或任何其他组合,我无法知道哪一个是正确的。

一份两位数的数字列表可能会更好。它不需要考虑太多,但仍然能够传达所需的信息。从现在开始,我决定把这个列表看作两位数的数字,而不是三位数的数字。

在这一点上,我仍然不确定如何在纸上解决这个问题,所以我决定试着倒着做。我写下了一个随机数字,然后从其中挑选了几对数字,试图重建我自己版本的问题。我决定仔细检查列表中的每个数字,并将数字写下来,就像它们被插入到某种数组中一样。

我意识到这样的方法不太实际,因为它仍然留下了模棱两可的空间。

在上面的例子中,很明显5在2之前,但如果我继续添加下一个数字中的数字,我不知道1应该在5之前还是之后。8也是同样的问题:我知道它应该在2之前,但数据没有说明它应该在1之前还是在5之后。

即使我有另一个数据点来消除线索的歧义--例如51--当然,它会告诉我1位于5和2之间的正确位置,但我已经感觉到,试图编写代码来解释数组中的数字切换是不现实的,而且可能有更简单的方法。

这样写出来让我意识到,某种数据结构的帮助将有助于解决这个问题。

所以现在的问题是,什么样的数据结构可以帮助建模这个问题?为了帮助做出这个决定,我考虑了问题中实际提供了哪些数据。使用上面的示例,数字列表如下所示:

列表中的每个数字都提供了有用的线索,但问题是很难跟踪每条线索最终是如何组合在一起的。我试着想出一种数据结构,既可以单独表示每条线索,也可以作为一个整体来表示。

有向图似乎很自然地适合表示这种信息。每个节点可以是一个数字,节点之间的边可以表示它们之间的关系。

我对用图表来解决这个问题感觉不错,但仍然有一些问题需要我去弄清楚。

我怎样才能知道图表何时有足够的信息来获得秘密数字呢?换句话说,我怎么知道我的答案是确凿的呢?

如果一个秘密号码有多个相同数字怎么办?这会毁了我的方法吗?

为了帮助回答这个问题,我使用了同样的技术,即创建问题的简化版本并向后处理。现在假设157是密码。节点之间需要多少条边才能确定1在5和7之前,5在7之前?答案是这个特殊图形的三条边。

在本例中,当边的数量等于节点的数量时,顺序是已知的。就这么简单吗?如果边数等于节点数,我们能知道秘密数是多少吗?

在本例中,答案是肯定的,但它并不适用于一般情况。通过创建更多的例子,我开始发现节点数和边数之间的关系。看看四位数和五位数的秘密数字在图表中是什么样子:

在画了几张之后,我可以开始看到一种模式出现了。随着节点数量的增加,边的数量也会增加,如1、3、6、10、15、21、28……。

寻找模式会很有帮助,因为这意味着有一个方程式可以用来表示问题的某些方面。这里的模式告诉我,为了知道什么时候我的答案可以被认为是确凿的,应该使用什么条件。这是表示该模式的等式,其中n是图中的节点数。

手绘图形有助于可视化解决此问题的方法,但我知道我还需要记住如何用代码表示图形。具体地说,就是如何以编程方式遍历图形以生成结果。

在盯着这些例子看了一段时间之后,我意识到这个图有一个明显且有用的属性。

具有最向外边缘的节点位于向外边缘较少的节点之前。此外,每个节点的边数正好相差一个。这意味着密码的第一个数字应该具有最向外的边缘,而最后一个数字将不会有任何向外的边缘。

这个属性对我来说是合乎逻辑的,而且可以很容易地转换成代码。

有多个相同数字的密码可能会导致我的方法出现问题。这是我在工作时担心的事情,因为不清楚如何知道应该将边放在哪两个节点之间。例如,看一下1030作为密码,想象一下按以下顺序给出的数字:

绘制图表有多种方法,因为有两种方法。仍然应该只有一种正确的方法。创建正确的图表可能取决于给出数字的顺序。我可能需要想出一些方法来回溯和重新连接节点,以便最终得到正确的图形。

可以比较正确和不正确的图表,以了解它们到底有多大不同。错误的图形有循环依赖关系:3位于橙色和蓝色0之前,但蓝色0位于3之前,这是相互矛盾的。

另一个不同之处在于,正确的版本是唯一满足上述属性的版本,其中每个节点的边数正好相差一个。对于使用图形建模的任何秘密数字,此属性应始终为真。

在这一点上,我决定搁置重复数字的问题。看起来这会打破我原本计划使用的方法。我认为有可能弄清楚,但还不清楚这个用例是否需要支持。

我现在的计划是将我的想法转化为代码,看看它是否会产生正确的答案。

这一部分很快就过去了,因为我已经对这个问题有了很好的理解,以及如何解决它的方法。我选择了Python并没有什么特别的原因,只是我觉得用一种我有一段时间没用过的语言会很有趣。

我不打算详细解释代码,但我想表达一下我在编写代码时的思考过程。

我的想法是循环列表中的每个数字来构建图表。到目前为止,图形只在纸上用圆圈和箭头直观地表示出来。现在的任务是使用代码表示图形。

至少有几种方法可以实现图表。我想出的方法最接近传统上所说的邻接矩阵,但有一些变化。

密钥日志=[319,680,180,690,#...]密钥日志中的位数:a,b,c=str(位数)adj_Matrix[(a,b)]=1 adj_Matrix[(a,c)]=1 adj_Matrix[(b,c)]=1。

在这里,该图由一个Python字典表示。字典中的每个键都是列表中当前号码的数字元组。这些值是用1硬编码的,表示元组中的两个数字之间有一条边。

值不那么重要,只要有两个数字表示边缘的关键字就行。换句话说,边缘由词典中词条的存在来表示。

请注意实际问题是如何提供三位数的列表的。使用两位数的数字是我最初为了帮助我思考这个问题而做的一个简化。三位数的数字并不能从根本上改变问题,它只是为每个数字提供了更多的数据。现在我不只知道数字a在数字b之前,我还知道数字a在c之前,b在c之前。

下一步是添加检查,以查看图表是否包含足够的信息来揭示秘密数字。正如我已经发现的,可以使用以下公式对条件进行建模:

对于键盘日志中的数字:#...。N_nodes=len(set([adj_matrix.key()中的边的位数,用于边中的位数]))n_edges=sum(adj_matrix.values())is_conducsive=n_node>;3 and(n_node*(n_node-1))/2==n_edges#...。

我发现添加nNodes>;3是必要的,否则程序会在第一次迭代后停止。

附注:我发现n*(n-1)/2==e+1也会产生正确的答案,而且迭代次数更少。然而,我认为它在某些情况下可能会留下模棱两可的空间,这取决于给出数字的顺序,所以我决定不把它作为我最终得到的解决方案的一部分。

一旦满足条件,就可以遍历图表以生成正确答案。

#...if is_conducsive:Nodes=[Edge[0]for edge in adj_Matrix]Counts_by_node=Reduced(count_digits,node,{})front_digits=排序(Counts_by_node,key=Counts_by_node.get,Reverse=True)[last_digit]=list(set([edge[1]for edge in adj_Matrix]).Difference(Nodes))Result=int(';';.Join(front_digits。

使用我奇怪/独特的图形表示法,只需创建一个包含每个元组中第一个元素的列表,就可以找到出边最多的节点。得到的列表将包含来自密码的每个数字的一个或多个,并将对其进行计数和排序。以下是一些打印语句,以帮助说明正在发生的情况:

打印(节点)#[';3';,';3';,';1';,';6';,';8';,';1';,';1';,';6';,';1';,';1';,';1';,';1';,';1';,';1';,';2';,';6';,';7';,';7';,';8';,';3';,';7';,';7';,';1';,';7';,';7';,';2';,';3';,';3';]打印(Counts_By_Nodes)#{';3';:6,';1';:5,';6';:4,';8';:2,';9';:1,';2';:3,';7';:7}打印(正面数字)#[';7';,';3';,';1';,';6';,';2';,';8';,';9';]。

结果出来之前的最后一步是得到密码的最后一位。它不包括在front_digits列表中,因为密码中的最后一个数字没有向外的边缘。它应该是图表中保留的唯一数字,但不是节点列表中的数字--在本例中为0。

所以密码是73162890。幸运的是,我不必担心重复的数字来解决这个特殊的问题,所以我很高兴我没有在这个问题上白白浪费时间。

我的回答到此为止,但下一步将是寻找改进我的答案的方法。现在我有了一个基线,我可以改进程序的所有方面,比如它的运行时、内存使用情况、代码的可读性,或者进行重构以处理特殊情况(如重复数字)。

我认为解决问题是非常个人化的。永远不会有一份供每个人遵循的繁琐步骤清单。我们的头脑是独一无二的,我们看待和解决问题的方式也是独一无二的,这真是太棒了!

很明显,这里不只有一种方法可以解决这个例子,但我希望我已经演示了一些我认为有用的技术。

从提出问题和对问题进行观察开始。有时候,这一步感觉就像我只是在转动轮子,或者在陈述显而易见的事情,但我发现它给了我开始工作所需的牵引力。

试着把问题缩小,然后解决那些问题。寻找简化所给信息的方法。这可以帮助我更容易地理解这个问题,它可以揭示出我可能没有考虑过的不同方面。

认识到你的想法很可能是错误的,而不是正确的。不要为此灰心丧气。失败的尝试可以为问题提供很多有价值的见解。

把想法写下来,把它们形象化。铅笔和纸是一种帮助我清晰思考的媒介。

尽量不要直接跳到代码中去,有时这是可以的,但也可能会让人分心。在代码中实现我的方法之前,我努力首先形成一个合理的概念模型。不过,有些时候我会做相反的事情。

想想边缘情况,但不要试图一下子解决所有问题。只要做个笔记,然后在必要的时候再去看看会很有帮助。

这几乎是不言而喻的,但实践/经验会产生很大的不同。例如,如果我从来没有听说过图形数据结构,我就不会想到将其应用于这个问题。所以,尽可能让练习和学习成为你日常工作流程的一部分。

虽然我们绝对可以相互学习,但我们也应该制定自己的技巧来推理我们面临的问题。这对我来说是一次有趣的练习--不仅是为了解决问题,也是为了分析我自己版本的解决问题所涉及的内容。自己试一试,看看你能学到什么。