Google面试问题解构了:《骑士的拨号器》(不可能是快速版)

Google面试问题解构了:《骑士的拨号器》(对数时间版)

这是我系列文章中的第二篇,其中列出了我曾经在Google上问过的最喜欢的面试问题,直到被泄露和禁止。 这篇文章是第一篇文章的续篇,因此,如果您还没有看过,建议您先阅读一下,然后再回来。 如果您不喜欢它,我仍然会尽力使此文章更有意义,但是我仍然建议您阅读第一篇有关某些背景的文章。

首先,是强制性的免责声明:虽然面试候选人是我的专业职责之一,但该博客代表了我的个人观察,我的轶事和个人见解。 请不要将此误认为是Google,Alphabet或任何其他个人或组织的官方声明。

抱歉,抱歉。 自从我出版了本系列的第一部分以来,我经历了很多(非常积极的)改变,结果一段时间以来,我的写作方式有些失落。 当事情公开时,我将尽我所能。

这篇文章去的方式超越我期望在工作面试中看到的。 我个人从未见过有人提出过这种解决方案,因为我的同事提到他见过的最优秀的候选人已经提出了更简单的解决方案,并花了其余的面试时间来尝试开发这种解决方案。 甚至那个候选人都失败了,而我只有经过反复的反复思考才得出了这个解决方案。 出于您的好奇心,我将与您分享这个问题,因为我认为这是数学与程序设计的完美结合。

顺便说一句,让我重新介绍一下这个问题:

问题

想象一下,您将骑士棋子放在电话拨号盘上。 此棋子以大写“ L”形移动:水平两步,然后垂直一步,或者水平一步,然后垂直两步:

假设您仅使用骑士能跳的键盘来拨打键盘上的键。 每次骑士落在某个键上时,我们都会拨该键并进行另一跳。 起始位置计为已拨。 您可以从一个特定的起始位置拨入N个跃点多少个不同的号码?

一篇文章的末尾,我们开发了一种解决方案,该解决方案可以在线性时间内解决此问题(取决于我们想要进行的跳数),并且需要恒定的空间。 很好 我曾经给那些能够开发和实施该职位的最终解决方案的候选人“征聘”。 但是,事实证明,如果我们使用一点数学就可以做得更好……

邻接表

一篇文章中解决方案的关键见解涉及将数字键盘以图表的形式显示,其中每个键是一个节点,骑士从该键的下一跳可能是该节点的邻居:

在代码中,可以这样表示:

出于多种原因,这是一个很好的表示。 首先,它很紧凑:我们仅表示图中存在的节点和边(为完整性起见,我包括数字5,但我们可以删除它而不会造成任何影响)。 其次,访问是高效的:我们可以通过映射查找在恒定时间内获得一组邻居,通过迭代该查找的结果,我们可以在一个特定节点的所有邻居上按与邻居数量成线性的时间进行迭代。 我们还可以通过使用集合而不是元组来轻松修改此结构,以在恒定时间内确定边的存在。

此数据结构称为邻接表 ,以显式列出相邻节点来表示边而命名。 到目前为止,这种表示法是最常见的表示图的方法,这主要是由于其在节点和边缘的线性空间复杂性以及其时间最优的访问模式。 大多数计算机科学家都会看一下这种表示,然后说:“打包起来,效果就差不多了。”

另一方面,数学家不会那么高兴。 是的,它体积小巧,运算速度快,但数学家(总的来说)并不像大多数计算机科学家和工程师那样从事实用的易用性业务。 一位计算机科学家可能会看一下这个图形数据结构,然后说:“这如何帮助我设计有效的算法?” 而数学家可能会看着它说:“这种表示方式如何使我能够使用其余的理论工具包?”

考虑到这个问题,数学家可能会对这种表示感到失望。 就个人而言,这种图形押韵的表示形式在我的数学教育中从未遇到过。 这对于编写算法很有用,但仅此而已。

作为矩阵的图

不过,还有另一种更富有成果的表示图的方法。 您会注意到图完全是关于节点之间的关系的。 对于邻接表,我们将每个节点与其连接的节点相关联。 为什么不关注节点对呢? 您可以问“给定一对节点,是否有一个边缘将它们连接起来?”,而不是询问“哪些节点通过边缘相互连接”。

如果这看起来像是“六分之一,六分之一”的情况,那就是。 但是第二种表达方式是神奇的,因为它使关注点在邻接列表表示中不可见:突然之间,我们对没有边的节点对非常感兴趣。 我们从所有可能的对开始,而不是从节点开始并仅计算相关 ,然后再决定它们是否相关。

我们可以重新构造邻接列表,如下所示。 请注意,对于每对(A,B),NEIGHBORS_MAP [A] [B]如果该对代表​​图形中的一条边,则将为1;否则为0:

我们为什么要这样做? 当然不会创建更有效的数据结构。 我们的空间复杂度已从与边的数量成比例,到可能的边的数量,这意味着N平方,其中N是节点数。 遍历邻居也变得更加昂贵:对于给定的节点,我们得到了一堆不相关的零,我们必须对其进行过滤。

另一方面,数学家对此感兴趣。 数学本科三年级以后的任何人都应该看一下,并立即说“那是矩阵!”

(为简洁起见,我在这里假定您对线性代数和矩阵乘法有足够的了解,可以随本博文一起学习。如果不了解,可以在这里找到不错的介绍。)

矩阵的妙处在于它们支持代数。 根据一些简单的规则,矩阵可以相加,相减和相乘。 这种特定表示形式缺乏紧凑性,它比抽象的易操纵性更胜一筹。

一旁

有点题外话:“好吧,很酷”,您可能会说,“我们将图形表示为矩阵。 该矩阵可以乘以另一个矩阵。 这与图形有什么关系? 谁在乎?” 您可能会意识到这是一个更有效的问题,答案是“还没有”。 本科生是我的目标读者,因此我有义务在继续之前将您置于正确的心境中,因为恐怕这样做可能比启发性更令人沮丧。

阅读完其余文章中提出的逻辑后,您可能会想问自己:“我到底该怎么想的?” 在阅读校样和教科书时,我肯定一次又一次地反应出这种感觉。 简短的答案是:您不是。 至少不是立即。 您学习的证明和定理越多,发现的图案和应用知识就越多。 我建议将此帖子视为另一个常识,希望以后再应用。

正事

好了,既然这已经不成问题了,那么让我们开始解决问题。 首先,我们将探讨该矩阵的结构。 (请注意,所有索引都是从零开始的偏移量。这与数学传统有所不同,但这是面向CS的文章,所以让我们一起去吧。)在此矩阵中,每一行代表每个键可访问的目标:行0具有位置4中的1表示您可以从0跳到4。位置9中的0表示您不能从0跳到9。

这些行也有含义。 当行表示,你可以相应的位置去,列代表怎样才能每个位置。 如果你仔细观察,你会发现,行和列看起来惊人地相似:每行中第i个位置是一样的在每列第i个位置。 这是因为该图是无向的:每个边都可以在两个方向上遍历。 结果,整个矩阵可以沿其主对角线翻转并保持不变(主对角线由行号和列号相等的位置形成)。

现在我们介绍了将图形表示为矩阵,它不再是一个算法对象,而是一个代数对象。 我们将要关注的特定代数运算是矩阵向量乘法。 将矩阵乘以向量会发生什么? 回想一下,将R行乘以C列矩阵AC长度列向量v的公式(对于具有C行和1列的矩阵的简称):

换句话说,这意味着可以通过取每一行,将该行的每个元素与向量中的相应元素相乘,并将分量值相加来计算所得的C长度矢量。 然后将结果放置在垂直的C比1矩阵中,或简称为C长度向量中。

乍一看似乎没有意思,但是实际上那里的代数关系是整个解决方案的症结所在。 考虑一下这意味着什么。 每行代表您可以从该行的相应键中获得的数字。 考虑到这一点,矩阵乘法不再是抽象的代数运算, 而是一种求和与拨号盘上给定键对应的目标值的方法

为了使含义清楚,请回顾我以前的文章中的递归关系:

标题:请记住, T代表您可以在N跳中从键K拨出的不同序列数

这无非就是拨号盘上给定键对应的目的地加权值之和! 这种框架忽略了不在图中的边缘,甚至没有在迭代中考虑它们,而面向矩阵的边缘包括它们,但仅作为乘以零的乘法,不会影响最终的总和。 这两个语句是等效的!

那么向量v在所有这一切中是什么意思? 到目前为止,我们几乎一直在谈论矩阵,而我们几乎忽略了向量。 我们可以选择我们想要的任何v ,但是我们希望选择一个在此计算中有意义的v 。 重复关系为我们提供了一个提示:在这种情况下,我们以T(K,0)开头,该值始终为1,因为在零跳情况下,我们只能拨打起始键。 让我们看看对于所有条目均为1的v会发生什么:

将转换矩阵与1向量相乘得到一个向量,其中每个元素对应于可以在一跳中拨打的号码计数。 再次相乘,我们可以:

现在,结果向量中的每个元素等于可以在2跳中从相应键拨打的数字计数! 我们刚刚开发了一个新的线性时间解决方案来解决骑士的拨号程序问题。 特别是:

对数时间

但是这个解决方案仍然是线性的。 我们需要一次又一次地将A与向量v相乘N次。 如果有的话,该解决方案实际上比我们在前一篇文章中开发的动态编程解决方案要慢,因为该解决方案需要不必要地乘以零。

但是,还有另一个可以使用的代数性质:矩阵可以相乘,并且可以相乘的任何数都可以取幂(为整数幂)。 我们的解决方案如下:

同样,我不会定义矩阵乘法,因此,如果您需要复习,请看一下这篇文章

我们如何计算A ^ N ? 自然,一种方法是将A自身重复乘以。 然而,这在某种程度上更浪费比矢量相乘:而不是一个向量由A乘以一次又一次,我们乘法A的列连连。 有更好的方法:通过平方求幂。

您可能知道,每个数字都有一个二进制表示形式。 如果您一直在学习计算机科学,那么您已经知道这是在硬件中表示数字的首选方式。 特别是,每个数字都可以表示为一系列位:

其中k是最大的非零位。 例如,二进制文件中的49是“ 110001”,或:

当我们在矩阵幂解中对N执行此扩展时,会发生一些有趣的事情:

回想一下,指数中的加法转换为其下的乘法

这导致总共k个矩阵乘法。 kN有何关系? k等于表示N所需的位数,您可能已经知道k等于log2(N) 。 不需要大量在N中线性增长的乘法,我们只需要对数个矩阵乘法即可! 这取决于一些有用的事实:

  • 幂为零的A是单位矩阵。 单位矩阵乘以单位矩阵可得出原始矩阵。 结果,如果任何一位为零,我们最终将乘以单位矩阵,就好像我们忽略了它一样。
  • 通过一次又一次地平方结果,我们可以将A乘以2的任意幂。 一个平方是一个时代一个A到4是A平方乘A平方,依此类推。

就是这个! 现在,我们有一个对数解。

尽管由于矩阵乘法的定义,该解决方案比以前的代码需要更多的代码,但它仍然非常紧凑:

包起来

从表面上看,这种解决方案似乎很棒。 它具有对数时间复杂度和恒定空间复杂度。 您可能会认为它确实没有比这更好的了,对于这个特定问题,您将是对的。

但是,这种基于矩阵求幂的方法有一个明显的缺点:我们需要将整个图形表示为一个(可能非常稀疏的)矩阵。 这意味着我们将必须显式地存储每个可能的节点对的值,这需要节点数量为平方的空间。 对于像这样的10节点图,这不是问题,但是对于更现实的图(可能具有数千个甚至数百万个节点),它变得不可行了。

更糟糕的是,我放弃的矩阵乘法实际上在行数上是三次 (对于正方形矩阵)。 最著名的矩阵乘法算法(例如Strassen或Coppersmith–Winograd)具有亚三次运行时间,但是要么需要极高的内存开销,要么具有恒定的因子,这些因子会抵消合理大小的矩阵的影响。 立方时间矩阵乘法对于大小在一万左右的图形开始变得不合理。

归根结底,这些限制在我心中都不重要。 说实话:您将在多大的现实图上进行计算? 请随时在评论中纠正我,但我个人认为该算法没有任何实际应用。

这个问题的主要目的是评估候选人的算法设计印章和编码技能。 如果考生使其接近我在这篇文章中讨论的东西在任何地方,他们很可能为谷歌工作SWE很多比我更有资格是...

如果你喜欢这篇文章, 叫好 或者 留下一个回应 我正在写这个系列来教育和启发人们,没有什么能让我感到与收到反馈一样好。 另外,如果这是您喜欢的东西,并且如果您一路走到这里,那么很有可能,请 跟随我 这是来自更多的地方。

此外,您还可以在此处找到可运行的代码,以用于本文以及上一篇文章。

下一步

禁止该问题后,我觉得我想开始问一个更简单的编程问题。 我到处搜寻了一个简单易懂的问题,有一个简单的解决方案,允许进行许多级别的跟进问题,并且与Google的产品有明显的联系。 我找到一个。 如果这听起来像您想阅读的内容,请继续关注…

From: https://hackernoon.com/google-interview-questions-deconstructed-the-knights-dialer-impossibly-fast-edition-c288da1685b8

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值