对于以下问题,如何改善我的代码?虽然有效,但我对它的外观不满意;我希望可以以某种方式使其保持递归状态,并在函数内部填充ans,并减少所拥有的return语句的数量。将其视为面试问题。

最坏的情况:\ $ O(2 ^ n)\ $?请验证并详细说明

空间复杂度:\ $ O(1)\ $


问题:

您有四个整数, abcd,目标是制作a == c
b == d,前提是您只允许在任何给定的转换下执行a+bbab+a
>
例如,如果a = 1b = 2c = 3d = 8
您可以从(1,2)开始,将其更改为(3,2),将其更改为(3,5),
,然后再次将其更改为(3,8)即可。



  static int ans = 0;
  public static void ablehelper(int a, int b, int c, int d){
      if(a != c && (b + a) > c){
          return;
      }
      if(b != d && (b + a) > d){
          return;
      }
      if(a == c && b == d){
          ans = 1;
          return;
      }
      ablehelper(a + b, b, c, d);
      ablehelper(a, b + a, c, d);

  }
  public static String able(int a, int  b, int c, int d){
      ablehelper(a, b, c, d);
      if(ans == 1){
          return "Able to generate";
      }else{
          return "Not table to generate";
      }
  }


更新:

As per one of the algorithms described below, I wrote this:


public static String betterSolution(int a, int b, int c, int d){
          while( c > a || d > b){
              if(c > d){
                  c = c-d;
              }else{
                  d = d-c;
              }
          }
          if( c == a &&  d == b){
              return "Able to generate";
          }else{
              return "Not able to generate";
          }
      }


评论

是否保证所有四个输入均为非负值?

@ 200_成功是;帮助我解决我所写内容的时间复杂性(我知道我想出的O(n)算法,但由于无法得出上述解决方案的最坏情况而感到沮丧)

bazang,您的新算法是否打算允许a,b,c或d中的任何一个等于0?如果是这样,它将无法正确处理输入a = 0,b = 1,c = 1,d = 1。它永远循环。但是,是的,这是对西蒙算法的忠实解释。但是,输入a = 1,b = 1,c = n,d = 1仍需要花费指数时间。要解决该问题,需要使用其他算法,例如我的答案中建议的算法。

#1 楼

这是一个有趣的问题!您让我们的聊天室长时间讨论了这个问题:)
您的方法很有趣,但是它有一个很大的缺陷:
您是从错误的方向开始的!
而不是从a和b开始,从c和d开始。问问自己这个问题:达到该数字的方法是什么?
请考虑以下问题:(我将使用符号a, b --> c, d
4, 5 --> 15, 19

15, 19之前必须采取什么步骤?是哪个号码?
15, x还是x, 19x, 19将是坚果,因为15小于19。因此,我们知道它是15, x。 x必须为4,因为15 + 4 =19。
通过继续减去具有最小数字的最大数字,我们可以找出所有可以产生这两个数字的可能输入。
15, 19
15, 4
11, 4
7, 4
3, 4
3, 1
2, 1
1, 1

因为a, b4, 5,所以我们已经在15, 4上看到了,因为4 < targetB是不可能的。
1, 2 --> 3, 8为例:
3, 8 <--- 8 is bigger, so decrease it by 3
3, 5 <--- 5 is bigger, so decrease it by 3
3, 2 <--- 3 is bigger, so decrease it by 2
1, 2 <--- 2 is bigger, so decrease it by 1
1, 1

等等,那是什么!? 1, 2在列表中!这是我们的幸运日!我们找到了匹配的a, b!因此,它将返回true
我将把实现代码的有趣部分留给您:)希望您理解这种方法。
关于时间复杂性:比当前的方法快得多。至于实时复杂性,请询问@rolfl。

评论


\ $ \ begingroup \ $
使用明显的实现,这会占用\ $ O(1)\ $空间,但是输入大小仍然是指数的。最坏的情况是a = 1,b = 1,c = n,d = 1。该算法将从\ $ n \ $倒数到\ $ 1 \ $。当然,这是实际解决问题的大小。但是,尚不清楚这是一个较低的界限。
\ $ \ endgroup \ $
–甘克罗
2014年5月11日4:45



\ $ \ begingroup \ $
@Ganko因为每个步骤都有一个解决方案,所以整个算法将是\ $ O(n)\ $,其中\ $ n \ $是\ $ c \ $和\ $ d \ $中的较大者。
\ $ \ endgroup \ $
– David Harkness
2014年5月11日下午5:10

\ $ \ begingroup \ $
是的,这是输入大小的指数。
\ $ \ endgroup \ $
–甘克罗
2014年5月11日下午5:38

\ $ \ begingroup \ $
@SimonAndréForsberg我仍然想知道我的问题最糟糕的情况是什么。是O(2 ^ n),如果是,为什么,如果否,为什么不呢?
\ $ \ endgroup \ $
–八藏
2014年5月11日下午6:25

\ $ \ begingroup \ $
如果d = 2 * c怎么办?在下一步中,您将得到c,c。我认为这个问题可以扩展到d = n * c,最终您将得到c,c组合。乍一看,我会说这些对不能解决。 (除了平凡解,其中c = 1)
\ $ \ endgroup \ $
–没人远离SE
2014年5月14日8:46



#2 楼

复杂性讨论

您问复杂性是否为\ $ O(2 ^ n)\ $。

如果这确实是一个面试问题,则要求您讨论解决方案的复杂性,您应该给出的答案大致如下:


计算的复杂性旨在描述解决方案如何随着您的变化而扩展输入数据量。通常,
两种不同类型的复杂度是键,时间复杂度和空间
复杂度。对于时间复杂度,如果复杂度为\ $ O(n)\ $并且
输入数据加倍,则时间将加倍。如果复杂度是
\ $ O(n ^ 2)\ $,那么当输入翻倍时,时间将增加四倍。

因为在这个问题上,只有4个输入值,没有办法有意义地讨论时间或空间的复杂性。
没有意义。输入数据永远不会缩放。


通过暗示复杂度为\ $ O(n ^ 2)\ $,您在复杂性知识上显示出差距。首先,您的问题中没有定义n ...这是什么?

此外,如果时间复杂度确实很重要/适用,则复杂度为\ $ O(2 ^ n)\ $很大。每次添加输入值时,执行时间都会加倍。因此,例如,如果您当前有4个输入值,并且需要4秒,那么5个输入值将花费8秒,6个输入值将花费16秒,而20个输入值将花费... ..差不多一天了。和24个值将花费一个月,而30个值将需要8.5年....

评论


\ $ \ begingroup \ $
如果有代表,我会为此-1。当然会有时间的复杂性! “在数学中,大O表示法描述了当参数趋于特定值或无穷大时函数的极限行为,通常是用更简单的函数表示。”在这种情况下,您可以根据输入值a,b,c和d提供O。是O(a ^ 2)吗? O(3 ^ c)?当然不是。但请注意,该算法对于能够(1,1,1000000,1),所以也许像O(max(c,d)/ min(a,b)-min(a,b))之类的东西。
\ $ \ endgroup \ $
– Claudiu
2014年5月11日下午1:38

\ $ \ begingroup \ $
还请注意:“ [...]素数测试和乘法算法的复杂度可以用两种不同的方法来度量:一种是针对要测试的整数,另一种是针对整数的数量。这些整数中的[位] [...]如果n是要测试素数的整数,则试验除法可以在Θ(n ^(1/2))算术运算中对其进行测试;但是如果n是整数中的位数正在测试整数的素性,它需要Θ(2 ^(n / 2))时间。在密码学和计算数论领域,更典型的是将变量定义为输入整数中的位数。
\ $ \ endgroup \ $
– Claudiu
2014年5月11日在1:39

\ $ \ begingroup \ $
@Claudiu-您是对的....但是,重点是您必须按照....的顺序定义复杂度,并且在计算中,对于n到0,O(n)是正常的。参考输入数据集的大小
\ $ \ endgroup \ $
–rolfl
2014年5月11日,凌晨1:40

\ $ \ begingroup \ $
@Claudiu-我邀请您进入第二个监控器聊天室,大约一个小时前在这里进行了讨论:chat.stackexchange.com/rooms/8595/the-2nd-monitor
\ $ \ endgroup \ $
–rolfl
2014年5月11日在1:41



\ $ \ begingroup \ $
如果输入是数字,则通常输入大小是编码这些数字所需的位数。对于操纵数字的算法,这是非常非常标准的。
\ $ \ endgroup \ $
–巴赫(G. Bach)
14年5月11日在11:45

#3 楼

编辑:这是基于对Simon的优化的线性时间算法!

算法

我们假定输入由非负整数组成。

Simon的算法是一项出色的改进,它可以很好地解决解决方案经常在两个选择之间“交替”出现的问题,但对于包含较长选择项的长“条纹”的解决方案,效果却很差。在解决方案a=1, b=1, c=n, d=1中可以观察到这一点。 Simon的算法将从\ $ n \ $倒数到\ $ 1 \ $,这是指数运行时间(根据表示输入所需的位)。

然后解决方案是“压缩这些路径。

和Simon的算法一样,我们从cd开始,并找出最大的路径。假设它是c(如果不是,只需应用对称推理)。因此,任何解决方案的上一步都必须是将c增加d。但是,与Simon的算法不同,我们将确定情况是多少次。这很简单:c就是d或c/d(使用整数除法)的整数倍。因此,我们不再对c-d, d进行递归,而是对c - d*(c/d), d进行递归。

但是现在我们可能可以“跳过”我们要寻找的起始位置(a,b)。不过,这很容易检测。最后,返回b==d and (c - a)%d == 0。最后,Simon没有提到的拐角处,当允许\ $ 0 \ $作为有效输入时,有必要考虑一下(Simon没有,所以他的算法很好) :c == d。如果是这种情况,那么实际上有两个有效的先前步骤:0, dc, 0。我们必须检查我们的解决方案是否为上述任何一种,然后简单地终止。

分析

在每一步中,我们两个数字之一至少缩小一半(最坏的情况是\ $ c = 2d-1 \ $)。当两个数字均为0时,我们的算法将在最坏的情况下终止。因此,在\ $ O(\ log c + \ log d)\ $时间之后,该算法必须终止。输入的大小是线性的。

可以使用循环或尾部递归在\ $ O(1)\ $空间中完成。

代码

最后,这是我在Java中的实现(经过编译并测试了一些输入)

public static boolean hasSolution(int a, int b, int c, int d){
    if(a < 0 || b < 0 || c < 0 || d < 0){
        throw new IllegalArgumentException("Negative inputs are not allowed");
    }

    if(c == 0 || d == 0){
        //This is a fixed point, an inescapable blackhole!
        return a == c && b == d;
    }

    while(true){
        if(a == c && b == d){
            //We're done!
            return true;
        }else if(c == d){
            //weird corner case
            return hasSolution(a, b, 0, d) || hasSolution(a, b, c, 0);
        }else if(c > d){
            int next = c - d*(c/d);
            if(next < a){
                //we're going to overshoot! 
                // Check if there exists a k such that 
                // a + k*d == c
                return b == d && (c - a)%d == 0;
            }else{
                //recurse!
                c = next;
            }
        }else{
            //No really, c is > d
            int temp;
            temp = a; a = b; b = temp;
            temp = c; c = d; d = temp;
        }
    }
}


原始答案(对OP算法的简要分析):

这是您的程序的一个非常糟糕的示例:
a=1, b=1, c=999999999, d=1

您的程序将继续执行第一个递归步骤,直到从a到c为止,然后返回true。

您的程序将在\ $ \ Omega(c)\ $时间运行,通常将其视为“指数”,因为它在所需位数上是指数代表输入。另外,您的程序将需要\ $ \ Omega(c)\ $空间,因为递归的第一个分支无法进行尾调用优化,因此每次迭代都将整个堆栈框架推到顶部。因此,您消耗的空间也与实际输入大小成指数关系。将此算法天真地转换为非递归(具有循环和堆栈)将无法解决此问题。

请注意,这仅适用于此输入。我不知道该算法的真正渐近运行时间是多少,因为怪异的输入具有怪异的行为,而且我不清楚真正的最坏情况到底是什么。本质上,您是在二叉树上进行深度优先搜索,其深度可能类似于\ $ O(c / b + d / a)\ $节点。因此,这绝对是空间上最坏的情况(如我的示例所示),但是运行时间尚不清楚。这样一棵树中的节点数可能高达\ $ 2 ^ {depth} \ $,所以也许有一个输入显示了\ $ \ Omega(2 ^ {c + d})\ $运行时间,(输入大小成倍增加),但我不清楚如何触发(如果可能)。我倾向于认为树中几乎没有太多的节点,因为树的“中间”变薄的速度比侧面要快得多(肯定总是有\ $ O(\ log { (c + d)})\ $较深。不过,您总是可以通过使答案为“否”来强制算法搜索整棵树(假设@janos的改进会在找到正确答案后终止)。
如果我不得不猜测,类似
a=2, b=2, c=2n+1, d=2n+1
表现出最坏的情况。

我相信递归树会像左右两边一样-大多数分支的大小为\ $ n \ $,接下来的向外分支的大小为\ $ n / 2 \ $,然后是\ $ n / 4 \ $,依此类推(减半),直到达到两个大小为\ $ 1的中间分支\ $。这给了我们大约\ $ 4n \ $个节点,与\ $ n \ $渐近相同,因此我猜想算法“ only”在最坏的情况下在指数时间和空间上运行。

Simon的答案无论如何都要好得多,而且也容易得多分析(尽管在最坏的情况下它仍然是指数)。

评论


\ $ \ begingroup \ $
为什么固定将\ $ n \ $定义为存储初始值所需的位数?您说的是“输入的大小”,但这只是大小的一种定义,我认为使用最少。
\ $ \ endgroup \ $
– David Harkness
2014年5月11日下午5:23

\ $ \ begingroup \ $
@DavidHarkness您能告诉我一个算法的运行时间与输入大小无关的地方吗?老实说,我在任何情况下都不知所措。
\ $ \ endgroup \ $
–甘克罗
2014年5月11日5:35



\ $ \ begingroup \ $
@DavidHarkness注意未能将单个数字\ $ n \ $视为仅实际的\ $ \ log n \ $位意味着存在背包问题(即NP-Complete)的多项式时间解相对于背包尺寸(输入中的单个整数)的线性时间运行的解决方案。这将是计算机科学领域的一项非凡突破!
\ $ \ endgroup \ $
–甘克罗
2014年5月11日下午5:42

\ $ \ begingroup \ $
您说我忽略了c == d的情况,但是您说我们的算法在两个数均为1时最坏的情况下终止,在这种情况下,您也忽略了以0、1开头的可能性案件。我假设所有值都为正整数(在这种情况下c == d)永远不会有解决方案。
\ $ \ endgroup \ $
–西蒙·福斯伯格
2014年5月11日在11:20

\ $ \ begingroup \ $
@SimonAndréForsberg是的,我确实在那儿进行了分析。我打算:一旦两个数字都为1,我们的算法最多就做固定的工作。但是,是的,如果您假设正整数(而不是像我那样是非负数,那么您的算法是正确的)。
\ $ \ endgroup \ $
–甘克罗
2014年5月11日12:32

#4 楼

使其成为纯递归

将实现转变为纯递归非常简单:

public boolean ablehelper(int a, int b, int c, int d) {
    if (a != c && (b + a) > c) {
        return false;
    }
    if (b != d && (b + a) > d) {
        return false;
    }
    if (a == c && b == d) {
        return true;
    }
    return ablehelper(a + b, b, c, d) || ablehelper(a, a + b, c, d);
}


您可以简化一下:

if (a > c || b > d) {
    return false;
}
if (a == c && b == d) {
    return true;
}
return ablehelper(a + b, b, c, d) || ablehelper(a, a + b, c, d);


递归,但要从另一个方向去

正如西蒙指出的,最好从另一个方向来思考:意识到之前的步骤得出解将具有以下一组值之一:


a = c , b = d - c, c , d - c
a = c - d, b = d , c - d, d

即在第一种情况下,您到达了通过使用(a, a + b)解决方案,在第二种情况下,通过采用(a + b, b)

朝这个方向思考将有所帮助,因为您无需探索将很多小数加起来的整个空间或可能无法达到正确的配对。这样一来,您将可以更快地找到解决方案:

if (a > c || b > d) {
    return false;
}
if (a == c && b == d) {
    return true;
}
if (c > d) {
    return ablehelper(a, b, c - d, d);
}
return ablehelper(a, b, c, d - c);


请记住,递归解决方案并不是此处的最佳方法。您不可避免地会在StackOverflowError中获得足够大的N,例如,在我的PC上,默认设置为9999足够大。您可以重构为使用循环而不是递归,这也适用于更大的(1, 1, N, 1)

以递归方式,从另一个方向出发,对类固醇

@Gankro的答案,请注意,当N时,而不是多次对c = X + Y * d进行递归,我们可以一步一步执行相同的操作:(c - d, d)。这样,可以通过以下方式改进实现的最后几行:

if (c > d) {
    return ablehelper(null, a, b, c - d * Math.max(1, c / d - 1), d);
}
return ablehelper(null, a, b, c, d - c * Math.max(1, d / c - 1));


我们不知道(c - d * Y, d)X,但是Y必须在Yc / d之间。因此,我们取c / d - 1,这样我们就不会过冲或减去过多。请确保c / d - 1的乘数至少为1。

复杂度

如果我能计算出正确的数值,我将再次发布。现在,我宁愿不说些愚蠢的话。我想说的是,也许最坏的情况是您无法使用快捷方式,而不得不从Math.maxc交替减法,例如使用一组斐波那契数,例如从d(c, d) = (21, 34)::

21, 34
21, 13
8, 13
8, 5
3, 5
3, 2
1, 2
1, 1


但是如何基于(1, 1)来表达和估计其复杂性,我不知道...

评论


\ $ \ begingroup \ $
最坏情况分析怎么办?
\ $ \ endgroup \ $
–八藏
2014年5月10日20:49