60题学会动态规划系列:动态规划算法第一讲

坚持就是胜利 - - 

文章目录

1.第N个泰波那切数

2.三步问题

3.使用最小花费爬楼梯

4.解码方法


1.第N个泰波那切数

力扣链接:力扣

泰波那契序列 Tn 定义如下: 

T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2

给你整数 n,请返回第 n 个泰波那契数 Tn 的值。

 首先我们分析一下题目,在n>=0的条件下也就是说都是正数,然后给了一个算泰博纳妾数的公式,这道题很简单,但是我们还是一步步带着大家来分析:

首先在我们以后做动态规划的题,都按照一下这个模板来编写:

1.状态表示

首先我们先把公式重新推导一下,让每边减3得到Tn = Tn-3 + Tn-2 + Tn-1 ,Tn就是第n个泰博纳妾数,根据前三个泰波纳妾数推导出来的第n个,所以说dp[i]的状态就是第i个泰波纳妾数

2.状态转移方程

dp[i] = dp[i-1] + dp[i-2] + dp[i-3]    因为第i个泰波纳妾数等于前三个泰波纳妾数的和,所以转移方程就是这个

3.初始化

我们初始化的目的是为了防止越界,如下图:

第0个泰波那切数的前三个都是非法的,所以我们必须给定第0个泰波纳妾数,同理dp[1]和dp[2]同样会越界,而题目又贴心的给了我们初始值,dp[0] = 0,dp[1] = 1,dp[2] = 1 

4.填表以及确定填表顺序

因为我们是知道左边的值,依靠左边的值推出来右边的值,所以顺序是从左向右

5.返回值

既然让我们返回第n个泰波纳妾数,而我们的状态表示dp[i]就表示第i个泰波纳妾数,所以返回dp[n]就是第n个泰波纳妾数

思路理解了我们直接写代码:

class Solution {
public:
    int tribonacci(int n) {
        //1.创建dp表
        //因为是从0开始的所以要多开一个位置
        vector<int> dp(n+1);
        //处理边界条件
        if (n==0) return 0;
        if (n==1||n==2) return 1;
        //2.初始化
        dp[0] = 0,dp[1] = dp[2] = 1;
        //3.状态转移方程
        for (int i = 3;i<=n;i++)
        {
            dp[i] = dp[i-1]+dp[i-2]+dp[i-3];
        }
        //返回值
        return dp[n];
    }
};

 首先创建dp表,由于是从0开始的所以要多开一个位置存储第n个泰波纳妾数,所以空间大小为n+1,因为第0个,第1个,第2个泰波那切数都给出来了,所以我们只需要从第3个泰波那切数开始计算,然后我们处理边界条件,如果不处理那么数组会直接越界,最后返回dp[n]即可。 

下面我们再讲解一下用滚动数组优化的方式,在上面的代码中我们要求第n个太波那契数列数是没必要将n之前的所有泰波那契数都保存的,我们只需要保存前三个就可以算出来了,所以我们只需要用四个变量就能解决这个问题:

 一开始d是第3个泰波那契数,经过滚动后下一次d变成了第4个泰波那契数,下面我们实现一下代码:

class Solution {
public:
    int tribonacci(int n) {
        //初始化前3个泰波那契数
        int a = 0;
        int b = 1;
        int c = 1;
        int d = 0;
        if (n==0) return 0;
        if (n==1||n==2) return 1;
        for (int i = 3;i<=n;i++)
        {
            d= a+b+c;
            a = b;
            b = c;
            c = d;
        }
        return d;
    }
};

 需要注意的是:滚动的时候应该是a向d去依次赋值,不能反过来反过来会导致原本c的值被覆盖。

2.三步问题

力扣链接:力扣

三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。

 首先题目说有n阶台阶,小孩一次可以上1阶或者2阶或者3阶,然后求有多少种上楼的方式,然后还说了题目结果会很大需要取模,下面我们就演示一下上楼梯的步骤:

 首先从地平线到第一个台阶只有一种方法,所以第一个台阶的方法数就是1.

到达第二个台阶可以从第0个台阶跨越2步到达(这是一种方法),也可以从第一个台阶跨越1步到达,由于要想从第一个台阶到第二个台阶就得先到第一个台阶,而到达第一个台阶的方法数是1,所以从第一个台阶到达第二个台阶的方法数为1*1==1.所以到达第二个台阶的总次数为2

到达第三个台阶可以直接从第0个台阶跨越3步到达,也可以从第一个台阶跨越2步到,还可以从第二个台阶跨越1步到达。首先从第0个台阶跨越3步到达方法数是1,从第一个台阶跨越需要知道到达第一个台阶的方法数所以是1*1==1,从第二个台阶跨越1步到达首先要计算到达第二个台阶的方法数所以是2*1 == 2,所以总次数为1 + 1 + 2 = 4.

到达第4个台阶可以从第一个台阶直接跨越3步,也可以从第二个台阶跨越2步,也可以从第3个台阶跨越一步。第一个台阶直接跨越3步的方法数为:1*1==1,第二个台阶跨越2步的方法数为2*1==2,第3个台阶跨越一步的方法数为4*1==4,所以总方法数为1+2+4==7

下面我们直接套模板:

1.状态表示

我们可以看到如果是第5个台阶的话那么有13种方法,而我们创建一个dp表将每个台阶的方法数放入,如下图:

所以dp[i]就表示第i个台阶的方法数。 

2.状态转移方程

可以通过图看到,第5个台阶的方法数是前3个台阶的方法数之和,所以状态转移方程为:

dp[i] = dp[i-1]  + dp[i-2] + dp[i-3]

3.初始化

由于第0,1,2个台阶会越界,所以我们直接给出方法数:dp[0] = 1,dp[1] = 1,dp[2] = 2,这里可能有人会有疑问,为什么到达地平线还有1个方法呢?在这里我们只能说要算第3个台阶的方法数dp[0]必须给1,这是靠分析题目得出来的。

4.填表

从左向右填表

5.返回值

dp[i]表示第i个台阶的方法数,所以返回dp[n]即可。

class Solution {
public:
    int waysToStep(int n) {
       const int MOD = 1e9+7;
       //创建dp表
       vector<int> dp(n+1);
       //解决边界问题
       if (n==0||n==1) return 1;
       if (n==2) return 2;
       dp[0] = 1,dp[1] = 1,dp[2] = 2;
       for (int i = 3;i<=n;i++)
       {
           dp[i] = ((dp[i-1]+dp[i-2])%MOD+dp[i-3])%MOD;
       }
       return dp[n];  
    } 
};

本题需要注意的是要对每次求出的台阶数取模,否则就会越界,我们直接用变量保存将要取模的数

然后在计算结果时每次相加就取模一次。

3.使用最小花费爬楼梯

力扣链接:力扣

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

 这道题有一个隐含条件,直接看实例1,我们大多数人都会认为楼顶就是数组的最后一个位置也就是20的位置,如果是20那么答案应该是10才对,为什么是15呢?因为楼顶其实是在数组最后一个位置的下一个位置:

了解了这个后我们讲一下如何计算:

首先能到达顶楼的位置有两个,要不然是顶楼-1的位置,要不然就是顶楼-2的位置,因为题目已经告诉了每次只能爬一个台阶或者两个台阶。所以我们只需要求出顶楼-1位置的最小花费和顶楼-2位置的最小花费的较小值,我们以实例1为例:

 由于题目已经告诉从0或1开始爬楼梯,所以到达0位置和1位置的最小花费是0.15是第一个台阶到达第一个台阶最小花费是0,然后15跨两步到楼顶需要支付15,所以15->楼顶的最小花费是15.

要计算20这个台阶的最小花费就要先知道哪两个台阶能到20这个位置,很明显从第0个台阶跨两步到20,到达第0个台阶的花费为0,从第0个台阶到第2个台阶的花费为10,所以10->20的花费为10,同理15->20的花费为15,那么到台阶2的最小花费就是10,然后从20->楼顶的花费是20,所以20->楼顶的最小花费是10+20==30。

取楼顶-1台阶的最小值和楼顶-2台阶的最小值相比较,最小的就是到楼顶的最小值,所以刚刚的实例2答案为15.

1.状态表示

dp[i]表示到达第i个台阶的最小花费

2.状态转移方程

dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])

3.初始化

因为题目告诉了从第0个台阶或者第1个台阶开始爬,所以之前到达0或1台阶的最小花费是0,所以dp[0]和dp[1]都等于0

4.填表

5.返回值

由于数组是从0开始的,size()的大小就是楼顶的位置,所以返回dp[size()]即可。

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        if (cost.size()<=0)
        {
            return 0;
        }
        //创建dp表
        int sz = cost.size();
        vector<int> dp(sz+1);
        //初始化
        dp[0] = dp[1] = 0;
        for (int i = 2;i<=sz;i++)
        {
            dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[sz];
    }
};

 上面我们是按照从前往后推的下面我们也可以换一个思想,从后往前推:

 比如我们现在在第7个台阶,那么我们支付了第7个台阶的费用后可以向后走一步也可以走两步,走一步到第8个台阶,第8个台阶花费100直接能到楼顶。走两步到第9个台阶,花费1直接就能到楼顶,这个时候我们第7个台阶到达楼顶的最小花费就是:第8个台阶和第9个台阶到达楼顶的最小花费的最小值,所以第7个台阶的最小花费就是1+1(+1是因为自己支付了费用要向后跳),以此类推从第7个台阶推到第0个台阶,又因为我们是从0或1出发,所以最后的返回值是0位置和1位置最小花费的最小值。

1.状态表示

dp[i]表示从i位置支付费用后到达楼顶花费的最小值

2.状态转移方程

dp[i] = min(dp[i+1],dp[i+2]) + cost[i]   (要记住我们从i位置起跳是需要花钱的所以要加上cost[i])

3.初始化

可以看到我们的转移方程是i+1和i+2,所以在距离楼顶的2个位置和1个位置的台阶是无法计算的因为越界了,并且从这两个位置起跳到楼顶只需要花费自身cost的费用。所以dp[楼顶-1] = cost[楼顶-1],dp[楼顶-2] = cost[楼顶-2]

4.填表

从右向左填表:

5.返回值

返回dp[0]和dp[1]的较小值

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        //创建dp表
        int sz = cost.size();
        vector<int> dp(sz);
        //初始化
        dp[sz-1] = cost[sz-1];
        dp[sz-2] = cost[sz-2];
        for (int i = sz-3;i>=0;i--)
        {
            dp[i] = min(dp[i+1],dp[i+2]) + cost[i];
        }
        return min(dp[0],dp[1]);
    }
};

4.解码方法

力扣链接:力扣

 首先我们要能读懂题,我们做的是解码工作,题目会给我们一个数字字符串,然后我们可以有多少种方法将数字转化为不同的字符顺序,要注意的是一旦有0作为前导就会解码失败,可以看实例3。(但是如果是两个字符,比如10那么可以解码为K那么就是一种方法).首先题目要求字符串的解码总数,那我们根据经验就可以得出状态表示,解码总数就是以字符串最后一个字符为结尾的总解码数。

1.状态表示

dp[i]表示以i字符为结尾的总解码数

2.状态转移方程

首先我们要知道dp[i]的方法数就要看s[i]这个字符能否解码,如果这个字符>='1'并且<='9'那么就是可以解码的,一旦这个字符可以解码就说明总方法数是dp[i-1],如下图:

 比如上图中dp[i]有三种方法分别是abc,acb,cba,当s[i]可以解码时就可以放在他们的后面,但是方法还是3种,因为我们要求的是以i为结尾的方法数而不是字符个数!当s[i]为0时那么在前面的所有努力都白费了,直接dp[i]等于0

当我们的s[i]可以和s[i-1]相结合变成一个新的字符时,那么就可以把s[i]和s[i-1]看成一个字符,那么总方法数就是前面i-2的方法数,所以是dp[i-2]

3.初始化

状态转移方程中只有0和1位置会越界,所以初始化0和1

首先如果第一个字符是>='1'&&<='9',那么dp[0]就是1,否则就是0

如果第2个字符可以解码,那么方法数就是i-1的方法数那就是1,如果第二个字符还可以和第一个字符结合,那么方法数就变成2

4.填表

已知0和1位置,所以从左向右填表:

5.返回值

返回以字符串最后一个字符为结尾的总方法数

class Solution {
public:
    int numDecodings(string s) {
      //创建dp表
      int sz = s.size();
      vector<int> dp(sz,0);
      //初始化
      dp[0] = s[0]!='0';
      //解决边界条件
      if (dp[0]==0) return 0;
      if (sz==1) return dp[0];
      //初始化
      if (s[1]>='1'&&s[1]<='9')
      {
          dp[1] = 1;
      }
      int t = (s[0]-'0')*10 + s[1]-'0';
      if (t>=10&&t<=26)
      {
          ++dp[1];
      }
      for (int i = 2;i<sz;i++)
      {
          if (s[i]>='1'&&s[i]<='9')
          {
              dp[i] += dp[i-1];
          }
          int t = (s[i-1]-'0')*10+s[i]-'0';
          if (t>=10&&t<=26)
          {
              dp[i] += dp[i-2];
          }
      }
      return dp[sz-1];
    }
};

 需要注意的是如果s[0]为0那么是不能解码的,当字符串长度为1时要返回dp[0]也就是第一个字符的方法数,否则我们下面的初始化dp[1]会越界。

下面我们讲一下能优化初始化的思路:

我们可以给dp表多开一个空间,这个位置放的值要视情况而定,如下图:

 我们在原先dp表的基础上多开了一个位置,那么这个位置该放多少呢?首先我们知道,第一个字符是一定要初始化的,第一个字符对应的dp表的位置再dp[1],我们将这个位置初始化使用动态转移方程的时候才不会越界(只要满足i-1,i-2不越界),dp[1]是我们自己初始化的,dp[2]需要状态转移方程计算,当第二个字符可以解码那么方法数是dp[i-1]也就是第一个字符的方法数1,然后第二个字符还可以和第一个字符结合,一旦结合解码数就变成dp[i-2],而dp[i-2]是我们的虚拟位置,如果我们第一个虚拟位置初始化为0,计算dp[i-2]的时候就会少一个方法数,所以1才是正确的,我们也可以再举一个例子:

 拿"2 0"来说,0位置的方法数是dp[i-1]+dp[i-2],由于是0所以dp[i-1]是0,dp[i-2]是我们的虚拟位置,2和0是可以结合的所以要有一种方法,如果虚拟位置为0就是0种方法就错了,所以虚拟位置应该为1.有了虚拟位置那么计算对应的字符串都需要-1,原来-1的要变成-2的字符,比如dp[i]的方法数是s[i-1]位置的字符的方法数。

class Solution {
public:
    int numDecodings(string s) {
      //创建dp表
      int sz = s.size();
      vector<int> dp(sz+1,0);
      //初始化
      dp[0] = 1;
      dp[1] = s[0]!='0';
      for (int i = 2;i<=sz;i++)
      {
          if (s[i-1]>='1'&&s[i-1]<='9')
          {
              dp[i] += dp[i-1];
          }
          int t = (s[i-2]-'0')*10+s[i-1]-'0';
          if (t>=10&&t<=26)
          {
              dp[i] += dp[i-2];
          }
      }
      return dp[sz];
    }
};

可以看到此方法不仅简化了代码而且让我们的初始化变的更简单。

猜你喜欢

转载自blog.csdn.net/Sxy_wspsby/article/details/130811998