【问题标题】:Climbing Stairs DP Problem base case concept爬楼梯 DP 问题基础案例概念
【发布时间】:2021-12-12 22:54:13
【问题描述】:

问题: 你正在爬楼梯。到达顶峰需要 n 步。 每次您可以跳 1 或 2 或 3 步。总共有多少种方法可以跳到顶部?

我的解释: 好吧,我正在考虑应用递归,因为我可以通过解决类似的子问题来找到解决方案,并且在这个过程中,会有许多重叠的子问题,所以我将排列数据结构来保存类似子问题的面额,这样我就不需要了解决相同的子问题两次。所以我使用的是自顶向下的DP方法。

我的疑问:

现在要构建解决方案,我需要一个基本案例,其中程序流程结束并返回到它的父节点(如果您将其可视化为一棵树)。所以我想的基本情况就像我在地板上时一样,在地面 0,所以我没有其他方法可以达到地面 0 状态,所以这是基本情况。

当 n=0 时,我应该返回 0 或 1,这是我的疑问吗?好吧,我已经编写了代码,所以当我返回 1 时代码工作,而不是在 n=0 时返回 0。那么为什么我在n=0的时候要返回1,这是什么原因呢?请帮忙!!!

我的代码:

#include <iostream>
using namespace std;

int climbing_ladders_topDown(int n, int k, int dp[]){
    //Base Case
    if(n==0){
        return 1;
    }

    //LookUp
    if(dp[n]!=0){
        return dp[n];
    }

    //Recursive Case
    int total_num_of_ways = 0;
    for(int jumps=1;jumps<=k;jumps++){
        if(n-jumps>=0){
            total_num_of_ways += climbing_ladders_topDown(n-jumps,k,dp);
        }
    }
    
    dp[n] = total_num_of_ways;
    return dp[n];
}

int main() {
    int num_of_stairs = 4;
    int num_of_jumbs = 3;
    int dp_arr[100] = {0};

    cout<<climbing_ladders_topDown(num_of_stairs,num_of_jumbs,dp_arr);
    
    return 0;
}

输出: 7

正确的代码流(感谢@appleapple):

#include <iostream>
using namespace std;

int climbing_ladders_topDown(int n, int k, int dp[]){

    //Base Case
    if(n==0){
        return 0;
    }

    //LookUp
    if(dp[n]!=0){
        return dp[n];
    }

    //Recursive Case
    int total_num_of_ways = 0;

    for(int jumps=1;jumps<=k;jumps++){
        
        if(n-jumps > 0){
            total_num_of_ways += climbing_ladders_topDown(n-jumps,k,dp);
        }
        
        if(n-jumps == 0){ // have reach the end or ground 0, base so no more move possible in downward direction
            total_num_of_ways += 1; 
        }
        
        if(n-jumps < 0){ //we can't move to -ve state/underground, because it doesn't exist
            total_num_of_ways += 0;
        }

    }
    
    dp[n] = total_num_of_ways;
    
    return dp[n];
}

int main() {

    int num_of_stairs = 4;
    int num_of_jumbs = 3;
    
    int dp_arr[100] = {0};

    cout<<climbing_ladders_topDown(num_of_stairs,num_of_jumbs,dp_arr);
    
    return 0;
}

【问题讨论】:

  • 因为total_num_of_ways += climbing_ladders_topDown(n-jumps,k,dp); 需要它是1。
  • 嗯,这是一个合乎逻辑的答案,因为这是我通过试验 n 错误得出的结论。但我想知道,为什么在 n=0 时,dp[0] = 1?
  • 是的dp[0] 应该是0。所以严格来说你的算法是错误的(对于n=0,它输出1)
  • 对不起,我编辑了。
  • 只是一个注释,dp[0]可以是1,如果你认为不移动是一种可能的方式。 (而0 表示它需要至少移动 1 步)

标签: c++ dynamic-programming


【解决方案1】:

因为您在这里要求它是1 (f(0) = 1)

for(int jumps=1;jumps<=k;jumps++){
   if(n-jumps>=0){
      total_num_of_ways += climbing_ladders_topDown(n-jumps,k,dp); // here
   }
}

如果你想要f(0)=0,因为递归到f(0) 不再有意义(没有可能的解决方案,就像f(-1))

这种情况的算法会变成

if(n<=0){ // not really necessary as implied inside the loop
   return 0; // not possible
}
///...
int total_num_of_ways = 0;
for(int jumps=1;jumps<=k;jumps++){
   if(n-jumps>0){
      total_num_of_ways += climbing_ladders_topDown(n-jumps,k,dp);
   }
   if(n-jumps==0){ // have reach the end, no more move possible
      ++total_num_of_ways; // you put this under n=0
   }
   // if(n-jumps<0){/*do nothing*/}
}

注意:f(0) = 0 或 f(0) = 1 提供了一些不同的含义。 (所以算法也变了)

  • f(0) = 1 表示不动是一种可能的解决方案。
  • f(0) = 0 表示至少需要走一步。
  • 两者都暗示一旦离开0(没有负面运动)就不可能回去了,顺便说一句。

【讨论】:

  • 如果在 n=0 时,返回 0 那么整个输出将为 0
  • @codosopher 不,因为循环不一样。
  • 如果你通过在 n=0 时返回 0 来运行相同的代码,你就会明白我在说什么
  • @codosopher 绝对不是 0?你在说什么? wandbox.org/permlink/lzl79V9oaWuJcH7h
  • 哦,因为你在最后添加了额外的不确定条件 if(n-jumps==0) 。但是,这实际上是什么意思,你能用外行的语言告诉你吗
【解决方案2】:

在这类计算路数的问题中,dp[0][..] 通常等于1,因为有 1 种方法可以跳 0 步,什么都不做。

对于您的问题,由于您已经发现这是一个 DP 问题,它可以通过 for 循环轻松解决,类似于具有不同基本案例起点的 Tribonacci 序列 (https://oeis.org/A000073):

#include <iostream>
using namespace std;

const int maxn = 1e3;

int main()
{
    int n; cin >> n;
    int dp[maxn+1];

    //base case
    dp[0] = 1; dp[1] = 1; dp[2] = 2;

    //dp
    for (int i = 3; i <= n; i++)
    {
        dp[i] = dp[i-1] + dp[i-2] + dp[i-3];
    }

    cout << dp[n];
}

测试:

Input : 3
Output : 4

说明:有 4 种方式:1 + 1 + 1、1 + 2、2 + 1、3。

你的递归和记忆 DP 状态的方式没有错,只是更粗。

需要明确的是,将dp[0] = 1 设置为基本情况只是一件简单且(某种)常规的事情。您完全可以将基本案例作为dp[1] = 1, dp[2] = 2, dp[3] = 4 并从dp[4] 开始。

代码的复杂度为O(n),但如果您需要更大的n 值,请查看此帖子:https://www.hackerearth.com/practice/notes/solving-linear-recurrence-relation/

【讨论】:

  • 嗯,我想知道原因,为什么 dp[0] = 1,而不是当 n=0 时为 0...我认为,这不是常规的事情,它可能会因问题而异
  • @codosopher 基本情况是指您尝试计算的函数的最简单的情况。由于这里的函数是dp[i] = dp[i-1] + dp[i-2] + dp[i-3],我们需要声明前3个基本情况。显然,这取决于功能,但更多取决于您选择的基本情况。在这里,dp[0] 回答了这个问题:0 步可以跳多少种方式。
  • 确实如此。这就是为什么,这一切都归结为基本情况和它返回的原因
  • @codosopher 是的,只是dp[0] = 1 在你第一次看到它时有点不合逻辑。而且您总是可以从不同的点开始,例如dp[1] 或dp[10]。这只是您在特定情况下定义为 base 的内容。例如,我给你序列10, 15, 20, 25,... 和函数f[i] = f[i-1]+5。要回答我为什么选择10作为基础的问题,只是我将10定义为基础情况,在这种情况下没有特别的原因或逻辑。
猜你喜欢
  • 2018-10-05
  • 2014-10-27
  • 1970-01-01
  • 1970-01-01
  • 2015-10-11
  • 2019-11-04
  • 1970-01-01
  • 2021-12-17
  • 2020-08-19
相关资源
最近更新 更多