【问题标题】:Finding all paths down stairs?找到所有下楼梯的路径?
【发布时间】:2011-07-03 05:30:27
【问题描述】:

我在面试中遇到了以下问题:

给定一个有 N 级台阶的楼梯,你每次可以走 1 或 2 级台阶。从下到上输出所有可能的方式。

例如:

N = 3

Output :
1 1 1
1 2
2 1

面试的时候,我只是说要使用动态规划。

S(n) = S(n-1) +1 或 S(n) = S(n-1) +2

然而,在采访中,我并没有为此写出非常好的代码。您将如何编写解决此问题的代码?

非常感谢!

【问题讨论】:

  • "有人可以帮我写代码吗?只是想学这个。"
  • @Crazy 因为我从未从阅读其他人的代码中学到任何东西。
  • @user470379 在这种情况下,您已经错过了很多。阅读其他人的代码是一个巨大的改进机会。
  • 至少 2 人的讽刺检测失败
  • @Andy:很确定他们只是认为你很愚蠢。

标签: c++ algorithm dynamic-programming


【解决方案1】:

我不会为你编写代码(因为这是一个很好的练习),但这是一个经典的动态规划问题。你在重复的正确轨道上;这是真的

S(0) = 1

因为如果你在楼梯的底部,那么只有一种方法可以做到这一点。我们也有这个

S(1) = 1

因为如果你高了一步,你唯一的选择就是往下走一步,此时你就处于底部。

从那里,很容易找到解决方案数量的递归。如果您考虑一下,您采取的任何步骤序列要么以一小步作为最后一步,要么以一大步作为最后一步。在第一种情况下,n - 1 个楼梯的 S(n - 1) 个解中的每一个都可以通过多走一步扩展为一个解,而在第二种情况下,n 个楼梯的每个 S(n - 2) 个解- 2个楼梯案例可以通过两个步骤扩展成一个解决方案。这给出了复发

S(n) = S(n - 2) + S(n - 1)

请注意,要计算 S(n),您只需要访问 S(n - 2) 和 S(n - 1)。这意味着您可以使用以下逻辑通过动态编程来解决这个问题:

  1. 创建一个数组S,其中包含 n + 1 个元素,索引为 0、1、2、...、n。
  2. 设置S[0] = S[1] = 1
  3. 对于从 2 到 n(含)的 i,设置 S[i] = S[i - 1] + S[i - 2]
  4. 返回S[n]

这个算法的运行时间是一个漂亮的 O(n) 与 O(n) 内存使用。

但是,可以做得比这更好。特别是,让我们看一下序列的前几项,它们是

 S(0) = 1
 S(1) = 1
 S(2) = 2
 S(3) = 3
 S(4) = 5

这看起来很像斐波那契数列,实际上你也许可以看到

 S(0) = F(1)
 S(1) = F(2)
 S(2) = F(3)
 S(3) = F(4)
 S(4) = F(5)

这表明,一般来说,S(n) = F(n + 1)。我们实际上可以通过对n 的归纳来证明这一点,如下所示。

作为我们的基本案例,我们有这个

S(0) = 1 = F(1) = F(0 + 1)

S(1) = 1 = F(2) = F(1 + 1)

对于归纳步​​骤,我们得到了

S(n) = S(n - 2) + S(n - 1) = F(n - 1) + F(n) = F(n + 1)

瞧!我们已经按照斐波那契数列编写了这个系列。这很棒,因为可以在 O(1) 空间和 O(lg n) 时间计算斐波那契数。有很多方法可以做到这一点。一个人使用的事实是

F(n) = (1 / √(5)) (Φn + φn)

这里,Φ是黄金比例,(1 + √5) / 2(约1.6),φ为1 - Φ,约-0.6。因为这第二项很快降为零,所以你可以通过计算得到第 n 个斐波那契数

(1 / √(5)) Φn

四舍五入。此外,您可以通过重复平方在 O(lg n) 时间内计算 Φn。这个想法是我们可以使用这个很酷的递归:

x0 = 1

x2n = xn * xn

x2n + 1 = x * xn * xn

您可以使用快速归纳论证证明这在 O(lg n) 时间内终止,这意味着您可以使用 O(1) 空间和 O(lg n) 时间来解决这个问题,这基本上是 比 DP 解决方案好。

希望这会有所帮助!

【讨论】:

  • @templatetypedef:这个答案很漂亮。但是,OP 不是在询问 print 每个步骤组合的算法,而不仅仅是查找步骤数吗?
  • @Padicus-天哪!我真的很傻。 :-( 我应该删除这篇文章吗?
  • 这是正确的答案,但对于错误的问题:) 我会说把它留在这里,也许它会在未来对其他人有所帮助。
  • @templatetypedef -- 绝对不要删除这个漂亮的帖子。 ;-) 它教会了我一些东西。
  • 如果它是对错误问题的正确答案,也许再问一个问题并将这个答案贴在上面?
【解决方案2】:

您可以将递归函数泛化为也采取已经采取的行动。

void steps(n, alreadyTakenSteps) {
    if (n == 0) {
        print already taken steps
    }
    if (n >= 1) {
        steps(n - 1, alreadyTakenSteps.append(1));
    }
    if (n >= 2) {
        steps(n - 2, alreadyTakenSteps.append(2));
    }
}

它不是真正的代码,更像是一个伪代码,但它应该给你一个想法。

【讨论】:

  • @Nikita:非常直接的答案。我喜欢!
  • +1 这是正确的(相当于我的答案,只是多几行:)
  • 这个解决方案不是展示了幼稚的方法吗?你考虑过Memoizing你的代码吗?
  • @bits 您不能输出复杂度低于2^n~ 2^n 解决方案。虽然这个解决方案还有一些改进的余地,但是记忆化在这里帮不了你。
  • @bits @Nikita:实际上是关于1.6^n 解决方案; 1.6^102^10 小一个数量级,因此记忆化确实会对您有很大帮助。不过,您必须更改代码以使其看起来像我的答案(让它返回一组可能的alreadyTakenSteps,而不是在最后打印出来)。
【解决方案3】:

@templatetypedef 给出了很好的答案——我把这个问题作为练习做了,然后在不同的路线上得到了斐波那契数:

这个问题基本上可以简化为Binomial coefficients的应用,这对于组合问题很方便:一次取k的n个事物的组合数(称为n选择k)可以通过等式找到

鉴于这一点以及手头的问题,您可以计算出解决方案蛮力(只需进行组合计数)。 “采取 2 步”的数量必须至少为零,最多可能为 50,因此组合的数量是 C(n,k) 的总和,对于 0

BigInteger combinationCount = 0;
for (int k = 0; k <= 50; k++)
{
    int n = 100 - k;
    BigInteger result = Fact(n) / (Fact(k) * Fact(n - k));
    combinationCount += result;
}

这些二项式系数的总和就是happens to also have a different formula

【讨论】:

  • 这很有趣但不回答问题,类似于templatetypedef的回答。
  • 我在回答已被接受时添加了此帖子。这能解决OP的问题吗?否。在这个问题的面试问题中,它是否相关且有帮助?绝对地!面试问题更多的是关于你如何找到解决方案,而不是脱口而出最终结果。不过感谢您的反对。
  • 我是反对者。它是相关的并且问题被标记为 interview-questions 的事实是不够的。可以添加大量不同的证明来证明该序列给出了斐波那契数。你只是在增加噪音。如果您有兴趣,请提出一个新问题,将其链接到此并回答。你会在那里得到我的支持。此外,为了保持一致性,所有离题的答案都会遭到我的反对(包括杰西和模板的答案)。在某种程度上,这不是你的错。盲目投票的人有过错。
【解决方案4】:

您的解决方案听起来不错。

S(n):
    If n = 1 return {1}
    If n = 2 return {2, (1,1)}
    Return S(n-1)x{1} U S(n-2)x{2}

(U 是Union,x 是Cartesian Product

记住这一点是微不足道的,并且会使其成为O(Fib(n))

【讨论】:

  • 太奇怪了,正确的解决方案逐渐消失,而甚至没有回答问题的答案却被投票赞成!至少 OP 知道他在问什么,并接受了一个解决问题的答案:-)(你已经有了我的 +1)
  • @BlueRaja - 我们可以使用高效的置换函数(在您使用 U 和 x 的步骤中)吗?还是会增加时间复杂度?有什么替代方法?
  • @rajya:我不确定你在问什么,但我可以告诉你,你无法真正改进这个算法:每次迭代都会产生一个新的输出。跨度>
  • @BlueRaja - 抱歉含糊不清。实际上我想知道我们如何在计算机程序中实现 U 和 x?我能想到的唯一方法是使用排列程序。我是初学者,请原谅我的幼稚。
  • @rajya:见@Nikita 的回答。但不是打印步骤,而是存储每个 S(n) 所需的步骤列表。
【解决方案5】:

其实,你可以证明攀登方式的数量就是斐波那契数列。很好的解释:http://theory.cs.uvic.ca/amof/e_fiboI.htm

【讨论】:

  • 这不能回答问题
  • 当然可以,这是一个常见的面试问题,这是预期的答案,它还解释了为什么动态编程解决方案有效并提供了另一种更有效的解决方案
  • 很好的回复!谢谢。如果可以的话,我会加 10。
  • 他问的是可能的序列,而不是可能的序列数。
  • 你是对的,我的错误,我仍然保留它,因为我认为它很有趣且相关
【解决方案6】:

解决问题和使用动态规划解决方案解决问题可能是两件不同的事情。

http://en.wikipedia.org/wiki/Dynamic_programming

一般来说,要解决一个给定的问题,我们需要解决问题的不同部分(子问题),然后将子问题的解决方案组合起来,达到一个整体的解决方案。通常,许多这些子问题实际上是相同的。动态规划方法力求每个子问题只求解一次,从而减少计算次数

这让我相信您想要寻找既是Recursive 又使用Memo Design Pattern 的解决方案。递归通过将问题分解为子问题来解决问题,而备忘录设计模式允许您缓存答案,从而避免重新计算。 (请注意,可能存在不是备忘录设计模式的缓存实现,您也可以使用其中之一)。

解决

我会采取的第一步是手动解决一些问题,使用不同或增加的 N 大小。这将为您提供一个模式来帮助您找出解决方案。从 N = 1 开始,一直到 N = 5。(正如其他人所说,它可能是斐波那契数列的一种形式,但我会在解决和理解问题之前自己确定这一点)。

从那里,我将尝试制作一个使用递归的通用解决方案。递归通过将问题分解为子问题来解决问题。

从那里,我会尝试将以前的问题输入缓存到相应的输出中,从而对其进行记忆,并制定一个涉及“动态编程”的解决方案。

也就是说,您的某个函数的输入可能是2, 5,而正确的结果是7。制作一些从现有列表或字典中查找的函数(基于输入)。它将查找使用输入 2, 5 进行的调用。如果没有找到,调用函数计算,然后存储并返回答案(7)。如果确实找到了,就不用计算了,返回之前计算的答案。

【讨论】:

    【解决方案7】:

    这是在非常简单的 CSharp 中解决此问题的简单方法(我相信您可以在几乎不更改 Java/C++ 的情况下将其移植)。 我给它增加了一点复杂性(增加了你也可以走 3 步的可能性)。如果需要,您甚至可以将此代码概括为“从 1 步到 k 步”,并在添加步骤时使用 while 循环(最后一个 if 语句)。

    我结合使用了动态编程和递归。动态规划的使用避免了之前每一步的重新计算;减少与调用堆栈相关的空间和时间复杂度。然而,它增加了一些空间复杂度 (O(maxSteps)),我认为与增益相比可以忽略不计。

    /// <summary>
    /// Given a staircase with N steps, you can go up with 1 or 2 or 3 steps each time.
    /// Output all possible way you go from bottom to top
    /// </summary>
    public class NStepsHop
    {
        const int maxSteps = 500;  // this is arbitrary
        static long[] HistorySumSteps = new long[maxSteps];
    
        public static long CountWays(int n)
        {
            if (n >= 0 && HistorySumSteps[n] != 0)
            {
                return HistorySumSteps[n];
            }
    
            long currentSteps = 0;
            if (n < 0)
            {
                return 0;
            }
            else if (n == 0)
            {
                currentSteps = 1;
            }
            else
            {
                currentSteps = CountWays(n - 1) + 
                               CountWays(n - 2) + 
                               CountWays(n - 3);
            }
    
            HistorySumSteps[n] = currentSteps;
            return currentSteps;
        }
    }
    

    你可以通过以下方式调用它

    long result;
    result = NStepsHop.CountWays(0);    // result = 1
    result = NStepsHop.CountWays(1);    // result = 1
    result = NStepsHop.CountWays(5);    // result = 13
    result = NStepsHop.CountWays(10);   // result = 274
    result = NStepsHop.CountWays(25);   // result = 2555757
    

    您可以争辩说,当 n = 0 时的初始情况,它可能是 0,而不是 1。我决定选择 1,但是修改这个假设是微不足道的。

    【讨论】:

      【解决方案8】:

      使用递归可以很好地解决问题:

      void printSteps(int n)
      {
         char* output = new char[n+1];
         generatePath(n, output, 0);
         printf("\n");
      }
      
      void generatePath(int n, char* out, int recLvl)
      {
         if (n==0)
         {
            out[recLvl] = '\0';
            printf("%s\n",out);
         }
      
         if(n>=1)
         {
            out[recLvl] = '1';
            generatePath(n-1,out,recLvl+1);
         }
      
         if(n>=2)
         {
            out[recLvl] = '2';
            generatePath(n-2,out,recLvl+1);
         }
      }
      

      主要是:

      void main()
      {
          printSteps(0);
          printSteps(3);
          printSteps(4);
          return 0;       
      }
      

      【讨论】:

        【解决方案9】:

        这是一个加权图问题。

        • 从 0 到 1 只能通过 1 种方式 (0-1)。
        • 您可以通过两种方式获得 2,从 0 和从 1(0-2、1-1)。
        • 您可以通过三种方式获得 3,从 1 和从 2(2 有两种方式)。
        • 您可以从 2 和 3 到 4 五种方式(2 有两种方式,3 有三种方式)。
        • 你可以达到 5 八种方式,...

        递归函数应该能够处理这个问题,从 N 向后工作。

        【讨论】:

          【解决方案10】:

          为此的完整 C-Sharp 代码

           void PrintAllWays(int n, string str) 
              {
                  string str1 = str;
                  StringBuilder sb = new StringBuilder(str1);
                  if (n == 0) 
                  {
                      Console.WriteLine(str1);
                      return;
                  }
                  if (n >= 1) 
                  {
                      sb = new StringBuilder(str1);
                      PrintAllWays(n - 1, sb.Append("1").ToString());
                  }
                  if (n >= 2) 
                  {
                      sb = new StringBuilder(str1);
                      PrintAllWays(n - 2, sb.Append("2").ToString());
                  }
              }
          

          【讨论】:

            【解决方案11】:

            基于 C 的后期答案

            #include <stdio.h>
            #include <stdlib.h>
            #define steps 60
            static long long unsigned int MAP[steps + 1] = {1 , 1 , 2 , 0,};
            
            static long long unsigned int countPossibilities(unsigned int n) {
                if (!MAP[n]) {
                   MAP[n] = countPossibilities(n-1) + countPossibilities(n-2);
                }
                return MAP[n];
            }
            
            int main() {
               printf("%llu",countPossibilities(steps));
            }
            

            【讨论】:

              【解决方案12】:

              这是一个 C++ 解决方案。这会打印给定楼梯数量的所有可能路径。

              // Utility function to print a Vector of Vectors
              void printVecOfVec(vector< vector<unsigned int> > vecOfVec)
              {
                  for (unsigned int i = 0; i < vecOfVec.size(); i++)
                  {
                      for (unsigned int j = 0; j < vecOfVec[i].size(); j++)
                      {
                          cout << vecOfVec[i][j] << " ";
                      }
                      cout <<  endl;
                  }
                  cout << endl;
              }
              
              // Given a source vector and a number, it appends the number to each source vectors
              // and puts the final values in the destination vector
              void appendElementToVector(vector< vector <unsigned int> > src,
                                         unsigned int num,
                                         vector< vector <unsigned int> > &dest)
              {
                  for (int i = 0; i < src.size(); i++)
                  {
                      src[i].push_back(num);
                      dest.push_back(src[i]);
                  }
              }
              
              // Ladder Problem
              void ladderDynamic(int number)
              {
                  vector< vector<unsigned int> > vecNminusTwo = {{}};
                  vector< vector<unsigned int> > vecNminusOne = {{1}};
                  vector< vector<unsigned int> > vecResult;
              
                  for (int i = 2; i <= number; i++)
                  {
                      // Empty the result vector to hold fresh set
                      vecResult.clear();
              
                      // Append '2' to all N-2 ladder positions
                      appendElementToVector(vecNminusTwo, 2, vecResult);
              
                      // Append '1' to all N-1 ladder positions
                      appendElementToVector(vecNminusOne, 1, vecResult);
              
                      vecNminusTwo = vecNminusOne;
                      vecNminusOne = vecResult;
                  }
              
                  printVecOfVec(vecResult);
              }
              
              int main()
              {
                  ladderDynamic(6);
                  return 0;
              }
              

              【讨论】:

                【解决方案13】:

                可能是我错了..但应该是:

                S(1) =0
                S(2) =1
                

                这里我们正在以这种方式考虑排列

                S(3) =3
                S(4) =7
                

                【讨论】:

                • 从 S(3) 的给定示例..我们不包括 3 作为可能的表达式..对于 4 可能的表达式是 1+1+1+1, 1+1+2 , 2+2, 1+2+1, 2+1+1, 3+1, 1+3
                猜你喜欢
                • 1970-01-01
                • 2020-11-09
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2015-02-22
                • 2017-12-24
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多