33.动态规划(1) | 斐波那契数、爬楼梯、使用最小花费爬楼梯

        今天开始新的一章,动态规划,也就是DP。学习了DP基础,了解到DP相关题目可以分为4个部分,分别是:

  1. 定义dp数组;
  2. 确定状态转移方程;
  3. 确定dp数组初始化方式;
  4. 确定遍历顺序。

这4部分的顺序性很重要,后面的步骤往往依赖于前面的。

        从今天的题目中学到DP的一个思想就是已知当前的位置,然后倒推当前位置的来源可能是哪些位置。


        第1题(LeetCode 509. 斐波那契数)很简单,是DP最基础的题目。DP定义方面,是将dp[i]定义为第i个斐波那契数。把前两个数字0和1存放在dp数组第0位和第1位,然后从第2位开始不断计算当前位的结果,即当前位的前面两位数字的和。所以状态转移方程是dp[i] = dp[i - 1] + dp[i - 2]。直到计算到第n位,将第n位的数字作为结果返回。

class Solution {
public:
    int fib(int n) {
        if (n == 0) {
            return 0;
        }
        int dp[n + 1];
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i <= n; ++i) {
            dp[i] = dp[i - 2] + dp[i - 1];
        }
        return dp[n];
    }
};

需要注意如果输入的n是0的话,就无法初始化dp数组第1位,会造成越界,所以要做特殊处理。

        使用dp数组会占用过多的空间,所以也可以仅用两个int数字来保存前两位的结果。

class Solution {
public:
    int fib(int n) {
        if (n == 0) {
            return 0;
        }
        int pre2 = 0, pre1 = 1;
        int ans = pre1;
        for (int i = 2; i <= n; ++i) {
            ans = pre2 + pre1;
            pre2 = pre1;
            pre1 = ans;
        }
        return ans;
    }
};

        第2题(LeetCode 70. 爬楼梯)自己曾经做过,但现在又忘记了具体的DP状态转移方程。看了部分题解后AC。首先定义方面,dp[i]表示上i阶楼梯的不同方法数量。因为每次只能上1阶或2阶,所以对于i阶楼梯,它的上一步要么是第i - 1阶,要么就是第i - 2阶。从第i - 1阶走1步走到第i阶,和从第i - 2阶走2步走到第i阶这两者是不重复的,因为他们的倒数第2步之后的阶梯已经不一样了,前者在第i - 1阶,后者在第i - 2阶。所以状态转移方程就是dp[i] = dp[i - 1] + dp[i - 2]。那么将前2位初始化为1和2,之后开始按照状态转移方程向后遍历,填充数字,指导第n阶为止,将第n阶的结果作为返回值返回。

class Solution {
public:
    int climbStairs(int n) {
        if (n == 1) {
            return 1;
        }
        vector<int> dp(n + 1);
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
};

与上一题一样,也可以只用2个变量来节省空间。

class Solution {
public:
    int climbStairs(int n) {
        if (n == 1) {
            return 1;
        }
        vector<int> dp(3);
        dp[1] = 1;
        dp[2] = 2;
        int ans = dp[2];
        for (int i = 3; i <= n; i++) {
            ans = dp[1] + dp[2];
            dp[1] = dp[2];
            dp[2] = ans;
        }
        return ans;
    }
};

        这道题可以扩展一下,每次能上的台阶数不再局限于1或2,而是变为[1, m]中任意一个数。对应的解法有两种,第2种是完全背包相关解法,放在后面再讨论(day 45)。第1种解法则可以延续这道题的思路,将dp从[1, m]都初始化。对于下标x的初始化,按照这道题的思路,x的上一步可能是[0, x - 1]中的任何一步,所以将dp[0]至dp[x - 1]全部相加就得到dp[x]。而[1, m]的初始化都依赖于dp[0],要将其初始化为1。

        初始化结束后,dp中[m + 1, n]的每个数y对应台阶的前一级台阶可能来自于[y - m, y - 1],所以将[y - m, y - 1]的dp值相加就得到dp[y]。

class Solution {
public:
    int climbStairs(int n) {
        if (n == 1) {
            return 1;
        }
        vector<int> dp(n + 1, 0);
        dp[0] = 1;
        int m = 2;
        int sum = dp[0];
        for (int i = 1; i <= m; ++i) {
            dp[i] = sum;
            sum += dp[i];
        }
        for (int i = m + 1; i <= n; ++i) {
            for (int j = i - m; j < i; j++) {
                dp[i] += dp[j];
            }
        }
        return dp[n];
    }
};

        第3题(LeetCode 746. 使用最小花费爬楼梯)自己AC,初看觉得很难,但也因为是DP章节所以想到用DP。定义方面,dp[i]表示到达第i阶楼梯所需要的最小花费。与上道题一样,对于第i阶楼梯,它的上一步要么是第i - 1阶楼梯,要么是第i - 2阶楼梯。所以已知了第i - 1阶和第i - 2阶楼梯的最小花费后,再令两者各自加上自己前进所需的花费,即cost[i - 1]和cost[i - 2],就得到了2种路线和对应的花费。从两者中取较小的一个当做第i阶的花费即可,所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]。dp前2个数字初始化为0,然后从第3个(下标2)开始遍历到“楼梯数组长度 + 1”个(下标楼梯数组长度),将其作为结果返回。

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int> dp(cost.size() + 1, 0);
        for (int i = 2; i <= cost.size(); ++i) {
            dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        return dp.back();
    }
};

        题解则是将dp[i]定义为到达第i阶楼梯后,再从第i阶楼梯出发所需要的最少花费。所以状态转移方程变为了dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]。在这种定义下,dp数组的前两个数字就需要填充为cost[0]和cost[1],然后从下标2一直遍历到下标楼梯数组长度为止。最后从dp数组的末尾两个数字中,取较小的一个当做答案,因为从这两个点出发都可以到达目标位置。

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int> dp(cost.size() + 1);
        dp[0] = cost[0];
        dp[1] = cost[1];
        for (int i = 2; i < cost.size(); ++i) {
            dp[i] = min(dp[i - 1], dp[i - 2])+ cost[i];
        }
        return min(dp[cost.size() - 1], dp[cost.size() - 2]);
    }
};

        由于这道题也只需要保存当前位置的前两个结果,所以也可以像前面两个题一样,用两个数字来代替dp数组,这里就不再写了。

        二刷:用了两行数组来分别表示到达当前台阶的最后一步是一级还是两级,但没必要,用一行数组表示两者的最小值就可以了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值