谷歌面试题The Knight's Dialer对数级时间复杂度解法

最近InfoQ上发了一篇Google面试官的文章翻译:一道泄漏并遭禁用的谷歌面试题,背后玄机全解析 - InfoQ微信公众号
原文链接:Google Interview Questions Deconstructed: The Knight’s Dialer

面试官在文末提到一位候选人,给出了一种他从未想到的解决方案,仅需对数级时间复杂度,并把这个问题留给读者。
本文的目的就是给出一种对数级时间复杂度算法,及其推导过程。

先简单描述一下问题:

把你的手机拨号页想象成一个棋盘。棋子走只能走“L”形状,横着两步,竖着一步;或者竖着两步,横着一步。


谷歌面试题The Knight's Dialer对数级时间复杂度解法_第1张图片
image

现在,假设你拨号只能像棋子一样走“L”形状。每走完一个“L”形拨一次号,起始位置也算拨号一次。问题:从某点开始,在N步内,你可以拨到多少不同的数字?


Pre:
开始之前,我们先看一眼用数学公式描述的部分答案:

谷歌面试题The Knight's Dialer对数级时间复杂度解法_第2张图片

其中,
S(1, n)表示从键1开始,跳n步可以拨到的号码数量;
F(x)是一个fibonacci函数:F(0)=0, F(1)=1, F(2)=1, F(3)=2, F(4)=3, F(5)=5, ...

Step0:
我们容易观察到以下结论:

  • S(1, n) = S(8, n - 1) + S(6, n - 1)
  • S(2, n) = S(7, n - 1) + S(9, n - 1)
  • S(3, n) = S(4, n - 1) + S(8, n - 1)
  • S(4, n) = S(3, n - 1) + S(9, n - 1) + S(0, n - 1)
  • S(5, n) = 1
  • S(6, n) = S(1, n - 1) + S(7, n - 1) + S(0, n - 1)
  • S(7, n) = S(2, n - 1) + S(6, n - 1)
  • S(8, n) = S(1, n - 1) + S(3, n - 1)
  • S(9, n) = S(2, n - 1) + S(4, n - 1)
  • S(0, n) = S(4, n - 1) + S(6, n - 1)

Step1:
我们观察上面的拨号页图片,很容易观察出数字布局是一张以键2和8连线为轴的轴对称图形,所以又可容易得出以下结论:

  • S(1, n) = S(3, n)
  • S(4, n) = S(6, n)
  • S(7, n) = S(9, n)

我们再观察0这个键,它只有4和6能够一步跳到,在脑海中想象一下,0漂浮到5的上方,二维的拨号页变成了三维的金字塔,它又成为以键4和6连线为轴的轴对称图形。于是我们又可以容易得到以下结论:

  • S(1, n) = S(7, n)
  • S(3, n) = S(9, n)
  • S(2, n) = S(8, n)

综合这6个结论:

  • S(1, n) = S(3, n) = S(7, n) = S(9, n)
  • S(4, n) = S(6, n)
  • S(2, n) = S(8, n)

其实得到这3个结论,这个问题的答案已经完成了一大半。

Step2:
接下来我们重点来计算S(1, n),先画一下树状展开图:

谷歌面试题The Knight's Dialer对数级时间复杂度解法_第3张图片
kd.png

先不显示第2个参数,利用Step1得到的结论简化这个树状图,并把它扩展到4层:
谷歌面试题The Knight's Dialer对数级时间复杂度解法_第4张图片
kd2.png

可以看出n为偶数,跳到0或1键上;n为奇数时,跳到8或6键上;
从0、1、8键往下跳有两种方案,从6键往下跳有三种方案。
可得出结论:
S(1, n) = S(1, n - 1) * 2,n为奇数

Step3:
观察简化后的树状图,可以发现S(6)是一个关键节点,它引发了某种突变,我们来计算S(6)在奇数层占的比例,记作P(6, i),由于S(0)占所在层的比例影响S(6),所以也计算S(0)在偶数层所在的比例,记作P(0, i),i为层数。
容易得到以下三个结论:

  1. S(1, n) = S(1, n - 1) * (2 + P(6, n - 1)),n为偶数
  2. 假设P(6, i) = m/n,则P(0, i+1) = m/(m+2n);
  3. 假设P(0, j) = x/y,则P(6, j+1) = (y+x)/2y

综合这两个结论:
假设P(6, i) = m/n,则P(6, i+2) = (m+n)/(m+2n)
可以看出m、n、m+n、m+2n是类似fibonacci数列的关系。
由于P(6, 1) = 1/2,所以可以得出结论:
P(6, i) = F(i + 1) / F(i + 2)
其中i为奇数,F为fibonacci函数
综合结论:

  1. S(1, n) = S(1, n - 1) * 2,n为奇数
  2. S(1, n) = S(1, n - 1) * (2 + P(6, n - 1)),n为偶数
  3. P(6, i) = F(i + 1) / F(i + 2),i为奇数

展开结论2:
S(1, n) = S(1, n - 1) * (2 + F(n) / F(n + 1))
S(1, n) = S(1, n - 1) * F(n + 3) / F(n + 1)
S(1, n) = S(1, n - 2) * 2 * F(n + 3) / F(n + 1)
S(1, n) = S(1, n - 3) * F(n + 1) / F(n - 1) * 2 * F(n + 3) / F(n + 1)
S(1, n) = S(1, n - 4) * 2 * F(n + 1) / F(n - 1) * 2 * F(n + 3) / F(n + 1)
S(1, n) = S(1, 0) * 2 ^ (n / 2) * F(5) / F(3) * F(7) / F(5) * ... * F(n + 3) / F(n + 1)
S(1, n) = S(1, 0) * 2 ^ (n / 2) * F(n + 3) / F(3)
由于S(1, 0) = 1,F(3) = 2,所以
S(1, n) = 2 ^ (n / 2 - 1) * F(n + 3)

Step4:
综合Step2和Step3的结论,得到最终的公式:

  1. S(1, 0) = 1
  2. S(1, n) = S(1, n - 1) * 2,n为奇数
  3. S(1, n) = 2 ^ (n / 2 - 1) * F(n + 3),n为偶数

类似的方法可求得从键6开始跳n步后的号码数量公式:

  1. S(6, 0) = 1
  2. S(6, n) = S(6, n - 1) * 2,n为偶数
  3. S(6, n) = 2 ^ ((n - 1) / 2) * F(n + 3),n为奇数

由于键8和键1的关系,得公式:

  1. S(8, 0) = 1
  2. S(8, n) = S(1, n - 1) * 2

同理,由于键0跟键6的关系,得公式:

  1. S(0, 0) = 1
  2. S(0, n) = S(6, n - 1) * 2

由于fibonacci函数的计算时间复杂度为O(logN),即对数级,所以这个算法整体计算时间复杂度为O(logN)。

你可能感兴趣的:(谷歌面试题The Knight's Dialer对数级时间复杂度解法)