动态规划专项
题目来源:https://leetcode-cn.com/study-plan/dynamic-programming/
题解参考自 LeetCode-Solution
来源:力扣(LeetCode)
Day1
斐波那契数
斐波那契数 (通常用 F(n)
表示)形成的序列称为 斐波那契数列 。该数列由 0
和 1
开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n
,请计算 F(n)
。
**提示: **
- 0 <= n <= 30
方法一:滚动数组
边界条件为F(0)和 F(1)
状态转移方程为 F(n) = F(n-1) + F(n-2)
class Solution {
public:
int fib(int n) {
if(n < 2) return n;
//滚动数组
int p = 0,q = 0,r = 1;
//q,r为 fib(0) 和 fib(1); p 用来储存答案的前一位,初始可以认为是fib(-1)
for(int i = 2; i <= n ; i++)
{
p = q;
q = r;
r = p + q;
}
return r;
}
};
复杂度分析
- 时间复杂度:O(n)O(n)。
- 空间复杂度:O(1)O(1)。
方法二:矩阵快速幂
方法一的时间复杂度是 O(n)O(n)。使用矩阵快速幂的方法可以降低时间复杂度。
首先我们可以构建这样一个递推关系:
[ 1 1 1 0 ] [ F ( n ) F ( n − 1 ) ] = [ F ( n ) + F ( n − 1 ) F ( n ) ] = [ F ( n + 1 ) F ( n ) ] \left[ \begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right] \left[ \begin{matrix} F(n)\\ F(n - 1) \end{matrix} \right] = \left[ \begin{matrix} F(n) + F(n - 1)\\ F(n) \end{matrix} \right] = \left[ \begin{matrix} F(n + 1)\\ F(n) \end{matrix} \right] [1110][F(n)F(n−1)]=[F(n)+F(n−1)F(n)]=[F(n+1)F(n)]
因此:
[
F
(
n
+
1
)
F
(
n
)
]
=
[
1
1
1
0
]
n
[
F
(
1
)
F
(
0
)
]
\left[ \begin{matrix} F(n + 1)\\ F(n) \end{matrix} \right] = \left[ \begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right] ^n \left[ \begin{matrix} F(1)\\ F(0) \end{matrix} \right]
[F(n+1)F(n)]=[1110]n[F(1)F(0)]
令:
M = [ 1 1 1 0 ] M = \left[ \begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right] M=[1110]
因此只要我们能快速计算矩阵 MM 的 n 次幂,就可以得到 F(n)的值。如果直接求取M^n ,时间复杂度是 O(n)O(n),可以定义矩阵乘法,然后用快速幂算法来加速这里 M^n 的求取。
class Solution {
public:
int fib(int n) {
if (n < 2) {
return n;
}
vector<vector<int>> q{{1, 1}, {1, 0}};
vector<vector<int>> res = matrix_pow(q, n - 1);
return res[0][0];
}
vector<vector<int>> matrix_pow(vector<vector<int>>& a, int n) {
vector<vector<int>> ret{{1, 0}, {0, 1}};
while (n > 0) {
if (n & 1) {
ret = matrix_multiply(ret, a);
}
n >>= 1;
a = matrix_multiply(a, a);
}
return ret;
}
vector<vector<int>> matrix_multiply(vector<vector<int>>& a, vector<vector<int>>& b) {
vector<vector<int>> c{{0, 0}, {0, 0}};
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;
}
};
第N个泰波纳契数列
泰波那契序列 Tn 定义如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2
给你整数 n
,请返回第 n 个泰波那契数 Tn
的值。
提示:
- 0 <= n <= 37
- 答案保证是一个32位整数,即
answer <= 2^31 - 1
方法:滚动数组
边界条件 F(0) F(1) F(2)
状态转移方程 F(n) = F(n-1) + F(n-2) + F(n-3)
class Solution {
public:
int fib(int n) {
if(n < 2) return n;
//滚动数组
int p = 0,q = 0,r = 1;
//q,r为 fib(0) 和 fib(1); p 用来储存答案的前一位,初始可以认为是fib(-1)
for(int i = 2; i <= n ; i++)
{
p = q;
q = r;
r = p + q;
}
return r;
}
};
Day2
爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例:
输入:n = 2 输出:2 解释:有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶
提示:
1 <= n <= 45
方法:滚动数组
边界条件 f(1) = 1 and f(2) = 2
我们用 f(x)
表示爬到当前阶数有多少中方法。我们知道走到第 x
个台阶有两种方法,即最后一步跨一步或者两步,那么走到当前台阶的方案数就是,爬到 x-1
级和 x-2
级台阶方案数之和
f
(
x
)
=
f
(
x
−
1
)
+
f
(
x
−
2
)
f(x) = f(x-1) + f(x-2)
f(x)=f(x−1)+f(x−2)
那么我们就得出了上式为状态转移方程
结果我们就可以由此不断推导得出答案
class Solution {
public:
int climbStairs(int n) {
//特判
if(n <= 2) return n;
//q记录f(x-2) ,k记录f(x-1),r记录f(x)
int q = 0, k = 1, r = 2;
for(int i = 3; i <= n; i++)
{
q = k;
k = r;
r = q + k;
}
return r;
}
};
使用最小花费爬楼梯
给你一个整数数组cost
,其中 cost[i]
是从楼梯第i
个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0
或下标为 1
的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例1:
输入:cost = [10,15,20] 输出:15 解释:你将从下标为 1 的台阶开始。 - 支付 15 ,向上爬两个台阶,到达楼梯顶部。 总花费为 15 。
示例2:
输入:cost = [1,100,1,1,1,100,1,1,100,1] 输出:6 解释:你将从下标为 0 的台阶开始。 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。 支付 1 ,向上爬一个台阶,到达楼梯顶部。 总花费为 6 。
方法:滚动数组
先找到边界条件,我们可以选择从第一二层开始
那么从第一层开始攀爬最小花费就为 f(1) = 0
,爬到第二层的最小花费 f(2) = min(cost[0],cost[1])
;这就是两个边界条件。
我们需要不断更新走到当前层数的最小花费,需要找到转移方程
那么走到当前层数同上题,有最后一步走一层和两层两种情况,那么我们只需要用 x-1
层走上来和从 x-2
层走上来花费少的方案来更新当前层数最小花费即可
f
(
x
)
=
m
i
n
(
f
(
x
−
1
)
+
c
o
s
t
[
x
−
1
]
,
f
(
x
−
2
)
+
c
o
s
t
[
x
−
2
]
)
f(x) = min(f(x-1)+cost[x-1] ,f(x-2)+cost[x-2])
f(x)=min(f(x−1)+cost[x−1],f(x−2)+cost[x−2])
我们得出该式子为状态转移方程
在代码中,我们用两个变量记录f(x-1) 和 f(x-2)
分别为p,q
然后不断滚动更新f(x)
即 r
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
if(cost.size() <= 2){
return min(cost[0],cost[1]);
}
//p用于做运算,q为上层最小花费,r为当前层最小花费
int p = 0, q = 0, r = min(cost[0],cost[1]);
for(int i = 2; i < cost.size(); i++)
{
//记录上层最小
p = q;
//记录当前层最小
q = r;
//更新下层最小
r = min(p+cost[i-1],r+cost[i]);
}
return r;
}
};
Day3
打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例:
输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
先找到边界条件
如果只有一个房屋,那么我们只能偷盗这一个房屋,所以返回最大金额为nums[0]
如果有两个房屋,那么我们就偷盗两个之中收益较大的,返回金额max(nums[0],num[1])
设置一个f[N]
数组用于储存(start,i)
区间最大收益,我们对最后一个房屋分析可以得知我们有两种方式到达该房屋,并可以知道对应收益
- 对
n
房屋打劫,代表不打劫第n-1
房间。收益为(start,n-2) 区间最大收益 + nums[n]当前房屋收益
- 不对
n
房屋打劫。收益为(start,n-1)区间最大收益
,即之前的最大收益。
我们需要最大收益,故取两者中最大一方。
f
(
i
)
=
M
a
x
(
f
(
i
−
2
)
+
n
u
m
s
[
i
]
,
f
(
i
−
1
)
)
f(i) = Max(f(i-2) + nums[i], f(i-1))
f(i)=Max(f(i−2)+nums[i],f(i−1))
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() == 1) return nums[0];
if(nums.size() == 2) return max(nums[0],nums[1]);
int f[400] = {0};
f[0] = nums[0], f[1] = max(nums[1],nums[0]);
for(int i = 2; i < nums.size(); i++)
{
f[i] = max(f[i-2] + nums[i] , f[i-1]);
}
int n = nums.size();
return f[n-1];
}
};
打家劫舍II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
> 输入:nums = [2,3,2] > 输出:3 > 解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1] 输出:4 解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
该题将房子按圈排,我们可以分为两种情况
- 偷窃
(1,n-1)
区间的房子 - 偷窃
(2,n)
区间的房子
因为偷窃了第一个房子那么最后一个房子不可能再被偷窃,反之同理。
那么我们就入原题一样,不过进行两次不同的 dp
,选取其中大的一种方案
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() == 1) return nums[0];
if(nums.size() == 2) return max(nums[0],nums[1]);
int n = nums.size();
int f[400] = {0},f2[400] = {0};
f[0] = nums[0], f[1] = max(nums[1],nums[0]);
for(int i = 2; i < nums.size()-1; i++)
{
f[i] = max(f[i-2] + nums[i] , f[i-1]);
}
f2[0] = 0, f2[1] = nums[1];
for(int i = 2; i < nums.size()-2; i++)
{
f2[i] = max(f2[i-2] + nums[i], f2[i-1]);
}
return max(max(f[n-1],f[n-2]),max(f2[n-2]+nums[n-1],f2[n-3]+nums[n-1]));
}
};