目录
一,爬楼梯
- class Solution {
- public:
- int climbStairs(int n) {
- int p = 0, q = 0, r = 1;
- for (int i = 1; i <= n; ++i) {
- p = q;
- q = r;
- r = p + q;
- }
- return r;
- }
- };
- class Solution {
- public:
- vector<vector<long long>> multiply(vector
> &a, vector> &b) { - vector<vector<long long>> c(2, vector
(2)); - for (int i = 0; i < 2; i++) {
- for (int j = 0; j < 2; j++) {
- c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j];
- }
- }
- return c;
- }
-
- vector<vector<long long>> matrixPow(vector
> a, int n) { - vector<vector<long long>> ret = {{1, 0}, {0, 1}};
- while (n > 0) {
- if ((n & 1) == 1) {
- ret = multiply(ret, a);
- }
- n >>= 1;
- a = multiply(a, a);
- }
- return ret;
- }
-
- int climbStairs(int n) {
- vector<vector<long long>> ret = {{1, 1}, {1, 0}};
- vector<vector<long long>> res = matrixPow(ret, n);
- return res[0][0];
- }
- };
- class Solution {
- public:
- int climbStairs(int n) {
- double sqrt5 = sqrt(5);
- double fibn = pow((1 + sqrt5) / 2, n + 1) - pow((1 - sqrt5) / 2, n + 1);
- return (int)round(fibn / sqrt5);
- }
- };
-
这里形成的数列正好是斐波那契数列,答案要求的 f(n)f(n) 即是斐波那契数列的第 nn 项(下标从 0 开始)。我们来总结一下斐波那契数列第 nn 项的求解方法:
n 比较小的时候,可以直接使用过递归法求解,不做任何记忆化操作,时间复杂度是 O(2^n),存在很多冗余计算。
一般情况下,我们使用「记忆化搜索」或者「迭代」的方法,实现这个转移方程,时间复杂度和空间复杂度都可以做到 O(n)。
为了优化空间复杂度,我们可以不用保存f(x−2) 之前的项,我们只用三个变量来维护 f(x)、f(x−1) 和f(x−2),你可以理解成是把「滚动数组思想」应用在了动态规划中,也可以理解成是一种递推,这样把空间复杂度优化到了 O(1)。
随着 n 的不断增大 O(n) 可能已经不能满足我们的需要了,我们可以用「矩阵快速幂」的方法把算法加速到 O(logn)。
我们也可以把 n 代入斐波那契数列的通项公式计算结果,但是如果我们用浮点数计算来实现,可能会产生精度误差。
二,打家劫舍
首先考虑最简单的情况。如果只有一间房屋,则偷窃该房屋,可以偷窃到最高总金额。如果只有两间房屋,则由于两间房屋相邻,不能同时偷窃,只能偷窃其中的一间房屋,因此选择其中金额较高的房屋进行偷窃,可以偷窃到最高总金额。
如果房屋数量大于两间,应该如何计算能够偷窃到的最高总金额呢?对于第 k (k>2) 间房屋,有两个选项:
偷窃第 k 间房屋,那么就不能偷窃第 k−1 间房屋,偷窃总金额为前 k−2 间房屋的最高总金额与第 k 间房屋的金额之和。
不偷窃第 k 间房屋,偷窃总金额为前 k−1 间房屋的最高总金额。
在两个选项中选择偷窃总金额较大的选项,该选项对应的偷窃总金额即为前 k 间房屋能偷窃到的最高总金额。
用 dp[i] 表示前 i 间房屋能偷窃到的最高总金额,那么就有如下的状态转移方程:
dp[i]=max(dp[i−2]+nums[i],dp[i−1])
边界条件为:
dp[0]=nums[0] 只有一间房屋,则偷窃该房屋
dp[1]=max(nums[0],nums[1]) 只有两间房屋,选择其中金额较高的房屋进行偷窃
最终的答案即为 dp[n−1],其中 n 是数组的长度。
- class Solution {
- public:
- int rob(vector<int>& nums) {
- if (nums.empty()) {
- return 0;
- }
- int size = nums.size();
- if (size == 1) {
- return nums[0];
- }
- vector<int> dp = vector<int>(size, 0);
- dp[0] = nums[0];
- dp[1] = max(nums[0], nums[1]);
- for (int i = 2; i < size; i++) {
- dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
- }
- return dp[size - 1];
- }
- };
上述方法使用了数组存储结果。考虑到每间房屋的最高总金额只和该房屋的前两间房屋的最高总金额相关,因此可以使用滚动数组,在每个时刻只需要存储前两间房屋的最高总金额。
- class Solution {
- public:
- int rob(vector<int>& nums) {
- if (nums.empty()) {
- return 0;
- }
- int size = nums.size();
- if (size == 1) {
- return nums[0];
- }
- int first = nums[0], second = max(nums[0], nums[1]);
- for (int i = 2; i < size; i++) {
- int temp = second;
- second = max(first + nums[i], second);
- first = temp;
- }
- return second;
- }
- };
-
时间复杂度:O(n),其中 n 是数组长度。只需要对数组遍历一次。
空间复杂度:O(1)。使用滚动数组,可以只存储前两间房屋的最高总金额,而不需要存储整个数组的结果,因此空间复杂度是O(1)。
三,三角形的最小路径和
本题是一道非常经典且历史悠久的动态规划题,其作为算法题出现,最早可以追溯到 1994 年的 IOI(国际信息学奥林匹克竞赛)的 The Triangle。时光飞逝,经过 20 多年的沉淀,往日的国际竞赛题如今已经变成了动态规划的入门必做题,不断督促着我们学习和巩固算法。
在本题中,给定的三角形的行数为 n,并且第 i 行(从 00 开始编号)包含了i+1 个数。如果将每一行的左端对齐,那么会形成一个等腰直角三角形,如下所示:
- [2]
- [3,4]
- [6,5,7]
- [4,1,8,3]