给定一对整数,执行达到目标N的最小操作次数

时间:2017-10-08 10:13:18

标签: algorithm math

给出一对数字(A,B)。 您可以执行操作(A + B,B)或(A,A + B)。 (A,B)初始化为(1,1)。 对于任何N> 0,找到你需要执行的最小操作次数(A,B),直到A = N或B = N

在glassdoor的采访摘要中遇到了这个问题。通过几种方法思考,在线搜索但无法找到解决此问题的任何文章/答案。我有一个暴力方法如下所示,但它必须遍历O(2 ^ N)路径,想知道是否有一个我没有看到的优雅解决方案。

def pairsum(N):
    A = 1
    B = 1

    return helper(N, A, B, 0)

def helper(N, A, B, ops):
    # Solution found
    if A == N or B == N:
        return ops

    # We've gone over, invalid path taken
    if A > N or B > N:
        return float("inf")

    return min(helper(N, A + B, B, ops + 1), helper(N, A, A + B, ops + 1))

2 个答案:

答案 0 :(得分:4)

给定目标编号N,可以在大约O(N log(N))基本算术运算中计算最小操作数(尽管我怀疑有更快的方法)。方法如下:

对于这个问题,我认为向前工作比向前工作更容易。假设我们试图达到正整数的目标对(a, b)。我们从(a, b)开始向后工作(1, 1),计算步数。这很简单的原因是从一对(a, b)回到(1, 1)只有一条路径:如果a > b,那么(a, b)对不能是第二个操作的结果,所以我们可能达到这一对的唯一方法是将第一个操作应用于(a - b, b)。同样,如果a < b,我们只能通过应用于(a, b - a)的第二个操作到达该对。那个案例a = b怎么样?好吧,如果a = b = 1,那就无所事事了。如果a = ba > 1,那么我们根本无法达到该对:请注意,这两个操作都将互质对整数与互质对整数相对应,所以如果我们从(1, 1)开始,我们永远不能达到一对整数除数大于1的整数。

这导致以下代码计算从(1, 1)(a, b)的步数,对于任何一对正整数ab

def steps_to_reach(a, b):
    """
    Given a pair of positive integers, return the number of steps required
    to reach that pair from (1, 1), or None if no path exists.
    """
    steps = 0
    while True:
        if a > b:
            a -= b
        elif b > a:
            b -= a
        elif a == 1:  # must also have b == 1 here
            break
        else:
            return None  # no path, gcd(a, b) > 1
        steps += 1
    return steps

看看上面的代码,它与用于计算最大公约数的欧几里德算法有很强的相似性,除了我们通过使用重复减法而不是通过欧几里德除法步骤直接进入余数时做得非常低效。 。因此,可以使用以下等效,更简单,更快的版本替换上述内容:

def steps_to_reach_fast(a, b):
    """
    Given a pair of positive integers, return the number of steps required
    to reach that pair from (1, 1), or None if no path exists.

    Faster version of steps_to_reach.
    """
    steps = -1
    while b:
        a, (q, b) = b, divmod(a, b)
        steps += q
    return None if a > 1 else steps

我留给你检查这两段代码是否相同:不难证明,但如果你不想拿出笔和纸,那么快速检查提示应该是令人信服的:< / p>

>>> all(steps_to_reach(a, b) == steps_to_reach_fast(a, b) for a in range(1, 1001) for b in range(1, 1001))
True

调用steps_to_reach_fast(a, b)需要O(log(max(a, b)))算术运算。 (这是根据欧几里德算法的标准分析得出的。)

现在直截了当地找到给定n的最小操作次数:

def min_steps_to_reach(n):
    """
    Find the minimum number of steps to reach a pair (*, n) or (n, *).
    """
    # Count steps in all paths to (n, a). By symmetry, no need to
    # check (a, n) too.
    all_steps = (steps_to_reach_fast(n, a) for a in range(1, n+1))
    return min(steps for steps in all_steps if steps is not None)

此功能可以快速运行到n = 1000000左右。让我们打印出前几个值:

>>> min_steps_to_reach(10**6)  # takes ~1 second on my laptop
30
>>> [min_steps_to_reach(n) for n in range(1, 50)]
[0, 1, 2, 3, 3, 5, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 6, 7, 7, 7, 7, 7, 7, 8, 7, 7, 7, 8, 8, 7, 8, 8, 8, 9, 8, 8, 8, 9, 8, 8, 8, 8, 8, 9, 8]

Online Encyclopedia of Integer Sequences处的搜索会快速生成序列A178047,它完全匹配我们的序列。序列描述如下:

  

考虑Farey树A006842 / A006843; a(n)=行的行   首先出现分母n(假设第一行标记为第0行)。

事实上,如果你看一下你的两个操作生成的树,从(1, 1)开始,你认为每一对都是一个分数,你会得到一些与Stern-Brocot树非常相似的东西( Farey树的另一个名称):每行的内容相同,但每行内的顺序不同。事实证明,它是Stern-Brocot tree in disguise

这个观察为min_steps_to_reach提供了一个易于计算的下界:很容易证明在Stern-Brocot树的i行中出现的最大整数是分子或分母是i+2和Fibonacci数。因此,如果n > Fib(i+2),则min_steps_to_reach(n) > i(以及n == Fib(i+2),那么min_steps_to_reach(n) 正好 i)。获得上限(或没有详尽搜索的确切值)似乎有点困难。以下是最糟糕的情况:对于每个整数s >= 0,需要n步的最小s步骤(例如,506是需要15步的第一个数字):

[1, 2, 3, 4, 7, 6, 14, 20, 28, 38, 54, 90, 150, 216, 350, 506, 876, 1230, 2034, 3160, 4470, 7764]

如果这里有一个模式,我没有发现它(但它实际上是OEIS上的序列A135510)。

答案 1 :(得分:1)

[在我意识到@ mark-dickinson回答之前我写过这篇文章;他的答案比我的好得多,但无论如何我都在提供参考资料]

如果你倒退,这个问题很容易解决。例如,假设N = 65:

  • 这意味着对于某些未知的x和y值,我们当前的对是{65,x}或{y,65}。

  • 如果{A,B}是前一对,则表示{A,A + B}或{A + B,B}等于{65,x}或{y,65} ,这给了我们4种可能的情况:

    • {A,A + B} = {65,x},这意味着A = 65。但是,如果A = 65,我们已经在之前的步骤中击中了A = N,我们假设这是A = N或B = N的第一步,所以我们放弃了这种可能性。

    • {A,A + B} = {y,65},表示A + B = 65

    • {A + B,B} = {65,x}表示A + B = 65

    • {A + B,B} = {y,65},这意味着B = 65。但是,如果B = 65,我们之前已经有了解决方案,我们也放弃了这种可能性。

因此,A + B = 65。有65种方法可以发生这种情况(实际上,我认为你可以忽略A = 0或B = 0的情况,并且通过对称选择B> A,但即使没有这些假设,解决方案也很容易)。

  • 我们现在检查所有65个案例。例如,让我们使用A = 25和B = 40。

  • 如果{C,D}是生成{25,40}的那对,则有两种可能的情况:

    • {C + D,D} = {25,40}所以D = 40且C = -15,这是不可能的,因为从{1,1}开始,我们永远不会得到负数。

    • {C,C + D} = {25,40}所以C = 25,D = 15.

  • 因此,{25,40}的“前身”必然是{25,15}。

  • 通过类似的分析,{25,15}的前身,我们称之为{E,F},必须具有以下属性:

    • {E,E + F} = {25,15},不可能,因为这意味着F = -10

    • {E + F,F} = {25,15}表示E = 10且F = 15.

  • 同样地,{10,15}的前身是{10,5},其前身是{5,5}。

  • {5,5}的前身是{0,5}或{5,0}。这两对是他们自己的前辈,但没有其他前辈。

  • 由于我们从未按此顺序命中{1,1},因此我们知道{1,1}永远不会生成{25,40},因此我们继续计算其他对{A,B},以便A + B = 65

  • 如果我们点击{1,1},我们会计算到达那里所需的步数,存储值,计算{A,B}的所有其他值,以便A + B = 65,并采取最低限度。

注意,一旦我们选择了A的值(因而是B的值),我们实际上是在做Euclid's Algorithm的减法版本,所以所需的步数是O(log(N) )。由于您执行N次这些步骤,算法为O(N * log(N)),远小于O(2 ^ N)。

当然,您可以找到快捷方式,使方法更快。

有趣的笔记

如果你从{1,1}开始,这里是你可以用k步骤生成的对(我们使用k = 0表示{1,1}本身),删除重复项后:

k = 0:{1,1}

k = 1:{2,1},{1,2}

k = 2:{3,1},{2,3},{3,2},{1,3}

k = 3:{4,1},{3,4},{5,3},{2,5},{5,2},{3,5},{4,3},{ 1,4}

k = 4:{5,1},{4,5},{7,4},{3,7},{8,3},{5,8},{7,5},{ 2,7},{7,2},{5,7},{8,5},{3,8},{7,3},{4,7},{5,4},{1, 5}

k = 5:{6,1},{5,6},{9,5},{4,9},{11,4},{7,11},{10,7},{ 3,10},{11,3},{8,11},{13,8},{5,13},{12,5},{7,12},{9,7},{2, 9},{9,2},{7,9},{12,7},{5,12},{13,5},{8,13},{11,8},{3,11} ,{10,3},{7,10},{11,7},{4,11},{9,4},{5,9},{6,5},{1,6}

注意事项:

  • 您可以分4步生成N = 7和N = 8,但不能生成N = 6,这需要5个步骤。

  • 生成的对数是2 ^ k

  • 达到给定N所需的最小步数(k)为:

N = 1:k = 0

N = 2:k = 1

N = 3:k = 2

N = 4:k = 3

N = 5:k = 3

N = 6:k = 5

N = 7:k = 4

N = 8:k = 4

N = 9:k = 5

N = 10:k = 5

N = 11:k = 5

结果序列{0,1,2,3,3,5,4,4,5,5,5,...}为https://oeis.org/A178047

  • 以k为单位生成的最高数字是(k + 2)和斐波纳契数,http://oeis.org/A000045

  • 以k步为单位可以达到的不同整数的数量现在是http://oeis.org/A293160的第(k + 1)个元素

  • 作为k = 20的例子:

    • 当k = 20

    • 时,有2 ^ 20或1048576对
    • 上述任何1048576对中的最高数字是17711,第22(20 + 2)斐波纳契数

    • 但是,您无法使用这些对到达所有前17711个整数。你只能达到11552,即A293160的第21(20 + 1)个元素

有关我如何解决此问题的详细信息,请参阅https://github.com/barrycarter/bcapps/blob/master/STACK/bc-add-sets.m