通过上篇文章,动态规划(零)入门概念相信大家已经对动态规划有了一些概念上的理解,那么如何运用动态规划去解决问题呢,首先要知道动态规划的解题步骤。
动态规划的步骤如下:
(1) 设计状态
(2) 写出状态转移方程
(3) 设定初始状态
(4) 执行状态转移
(5) 返回最终的解
下面就通过实战来进入到动态规划的学习当中。
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
class Solution {
public:
int climbStairs(int n) {
int dp[n+5];
memset(dp, 0, sizeof(dp));
dp[0] = dp[1] = 1;
for(int i = 2; i <= n; ++i){
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n];
}
};
(1) 设计状态dp[i],表示爬到第 i 阶时有多少种方法。
(2) 状态转移方程
d
p
[
i
]
=
{
1
,
i
=
0
1
,
i
=
1
=
d
p
[
i
−
1
]
+
d
p
[
i
−
2
]
,
i
≥
2
dp[i] =
因为第0层台阶和第0层台阶肯定就一种可能性。而到了第二层,则是由前两层状态转移过来的。
(3) 初始状态按照状态转移方程来设置即可。
(4) 执行状态转移则是用循环执行i ≥ 2 \geq 2 ≥2的部分。
(5) 返回最终的解就是返回dp[n]。
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。
class Solution {
public:
int fib(int n) {
int dp[n + 2];
memset(dp, 0, sizeof(dp));
dp[0] = 0;
dp[1] = 1;
for(int i = 2;i <= n; ++i){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};
(1) 状态转移方式,设计的状态与爬楼梯一致,详情可参考爬楼梯的题解。
泰波那契序列 Tn 定义如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2
给你整数 n,请返回第 n 个泰波那契数 Tn 的值。
class Solution {
public:
int tribonacci(int n) {
int dp[40];
memset(dp, 0, sizeof(dp));
dp[0] = 0;
dp[1] = 1;
dp[2] = 1;
for(int i = 3; i <= n; ++i){
dp[i] = dp[i-1] + dp[i-2] + dp[i-3];
}
return dp[n];
}
};
(1) 设计状态dp[i],表示爬到第 i 阶时有多少种方法。
(2) 状态转移方程
d
p
[
i
]
=
{
0
,
i
=
0
1
,
i
=
1
1
,
i
=
2
=
d
p
[
i
−
1
]
+
d
p
[
i
−
2
]
,
i
≥
3
dp[i] =
状态转移方程题目中已经给出公式
(3) 初始状态按照状态转移方程来设置即可。
(4) 执行状态转移则是用循环执行i ≥ 3 \geq 3 ≥3的部分。
(5) 返回最终的解就是返回dp[n]。
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n = cost.size();
int dp[n+1];
memset(dp, 0, sizeof(dp));
dp[0] = 0;
dp[1] = 0;
for(int i = 2; i <= n; ++i){
dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]);
}
return dp[n];
}
};
(1) 设计状态dp[i],表示爬到第i层的最小费用。
(2) 状态转移方程
d
p
[
i
]
=
{
0
,
i
=
0
1
,
i
=
0
1
,
i
=
2
=
m
i
n
(
d
p
[
i
−
1
]
+
c
o
s
t
[
i
−
1
]
,
d
p
[
i
−
2
]
+
c
o
s
t
[
i
−
2
]
)
,
i
≥
2
dp[i] =
一开始出发的第0层和第1层,所以为0层,大于等于2层的费用是由低于两层的费用加上从当层爬上来的费用转移过来的。
(3) 初始状态按照状态转移方程来设置即可。
(4) 执行状态转移则是用循环执行i ≥ 2 \geq 2 ≥2的部分。
(5) 返回最终的解就是返回dp[n]。
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
int dp[n+1];
memset(dp, 0, sizeof(dp));
if(n == 1){
return nums[0];
}
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for(int i = 2; i < n; ++i){
dp[i] = max(nums[i] + dp[i-2], dp[i-1]);
}
return dp[n-1];
}
};
(1) 设计状态dp[i],表示偷取编号0~i的房子,最大能偷多少。
(2) 状态转移方程
d
p
[
i
]
=
{
n
u
m
s
[
0
]
,
i
=
0
m
a
x
(
n
u
m
s
[
0
]
,
n
u
m
s
[
1
]
)
,
i
=
1
=
m
i
n
(
n
u
m
s
[
i
]
+
d
p
[
i
−
2
]
,
d
p
[
i
−
1
]
)
,
i
≥
2
dp[i] =
一开始只偷取标号为0的房子,那就只有一种可能性。
如果偷取标号0和1的房子,那么就只有偷取0号房子或者偷取1号房子。
如果偷取标号大于等于2的房子,那么就有两种可能性,一种是偷取当前标号的房子,一个是不偷去。
(3) 初始状态按照状态转移方程来设置即可。
(4) 执行状态转移则是用循环执行i ≥ 2 \geq 2 ≥2的部分。
(5) 返回最终的解就是返回dp[n-1]。(n 等于 1的时候例外,因为n == 1的情况下无法初始化状态,直接返回nums[0]即可)
给你一个整数数组 nums ,你可以对它进行一些操作。
每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。
开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
int n = nums.size();
if(n == 1){
return nums[0];
}
int hash[10010];
int max0 = 0;
memset(hash, 0, sizeof(hash));
for(int i = 0; i < n; ++i){
max0 = max(nums[i], max0);
hash[nums[i]] += nums[i];
}
int dp[10010];
memset(dp, 0, sizeof(dp));
dp[0] = 0;
dp[1] = hash[1];
for(int i = 2; i <= max0; ++i){
dp[i] = max(dp[i-2] + hash[i], dp[i-1]);
}
return dp[max0];
}
};
(1) 用哈希表记录从0到最大值的点数和为多少,比如数组中有10个1,那么hash[1] = 10。
(2) 后续思路与打家劫舍一致,没有变化。
给你整数 zero ,one ,low 和 high ,我们从空字符串开始构造一个字符串,每一步执行下面操作中的一种:
以上操作可以执行任意次。
如果通过以上过程得到一个 长度 在 low 和 high 之间(包含上下边界)的字符串,那么这个字符串我们称为 好 字符串。
请你返回满足以上要求的 不同 好字符串数目。由于答案可能很大,请将结果对 109 + 7 取余 后返回。
class Solution {
int mod = 10e8 + 7;
public:
int countGoodStrings(int low, int high, int zero, int one) {
int dp[high+1];
memset(dp, 0, sizeof(dp));
dp[zero]++;
dp[one]++;
int res = 0;
for(int i = min(zero, one); i <= high; ++i){
if(i - zero >= 0){
dp[i] = (dp[i] + dp[i - zero]) % mod;
}
if(i - one >= 0){
dp[i] = (dp[i] + dp[i - one]) % mod;
}
}
for(int i = low; i <= high; ++i){
res = (res + dp[i]) % mod;
}
return res;
}
};
(1) 解题思路与爬楼梯一致。
(2) 为数不多的差距就是状态是由长度为i - 1和i - 2转移过来的变成了由i - zero, i - one转移过来的。
给你一个下标从 0 开始的二维整数数组 questions ,其中 questions[i] = [pointsi, brainpoweri] 。
这个数组表示一场考试里的一系列题目,你需要 按顺序 (也就是从问题 0 开始依次解决),针对每个问题选择 解决 或者 跳过 操作。解决问题 i 将让你 获得 pointsi 的分数,但是你将 无法 解决接下来的 brainpoweri 个问题(即只能跳过接下来的 brainpoweri 个问题)。如果你跳过问题 i ,你可以对下一个问题决定使用哪种操作。
请你返回这场考试里你能获得的 最高 分数。
class Solution {
public:
long long mostPoints(vector<vector<int>>& questions) {
int n = questions.size();
long long dp[200010];
memset(dp,0,sizeof(dp));
for(int i = n-1; i >= 0; --i){
if(i == n-1){
dp[i] = questions[i][0];
continue;
}
int points = questions[i][0];
int brainpower = questions[i][1];
dp[i] = max(dp[i+1], points + dp[min(n, i + brainpower + 1)]);
}
return dp[0];
}
};
(1) 设计状态dp[i],表示解决i ~ n-1号题目的最大分。
(2) 状态转移方程
d
p
[
i
]
=
{
q
u
e
s
t
i
o
n
s
[
i
]
[
0
]
,
i
=
n
−
1
m
a
x
(
d
p
[
i
+
1
]
,
d
p
[
i
+
b
r
a
i
n
p
o
w
e
r
+
1
]
+
p
o
i
n
t
s
)
,
i
≤
n
−
2
dp[i] =
初始化从n - 1开始,然后一直倒序遍历即可。
(3) 初始状态按照状态转移方程来设置即可。
(4) 执行状态转移则是用循环执行i ≤ n − 2 \leq n - 2 ≤n−2的部分。
(5) 返回最终的解就是返回dp[0]。
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
‘A’ -> “1”
‘B’ -> “2”
…
‘Z’ -> “26”
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:
注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。
题目数据保证答案肯定是一个 32 位 的整数。
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
int dp[105];
memset(dp, 0, sizeof(dp));
for(int i = 0; i < n; ++i){
if(i == 0){
dp[i] = (s[i] == '0') ? 0 : 1;
} else{
if(s[i] != '0'){
dp[i] = dp[i-1];
}
if((s[i-1] == '1' || s[i-1] == '2')){
int val = (s[i-1] - '0') * 10 + (s[i] - '0');
if(val <= 26){
if(i == 1){
dp[i]++;
}
else{
dp[i] += dp[i-2];
}
}
}
}
}
return dp[n-1];
}
};
(1) 设计状态dp[i],表示到第i个字符所能表示解码种数。
(2) 状态转移方程
d
p
[
i
]
=
{
1
,
i
=
0
并且
s
[
i
]
=
′
0
′
0
,
i
=
1
并且
s
[
i
]
=
′
1
′
此时需要分类讨论情况了如果
s
[
i
]
不为
0
的话,此时
d
p
[
i
]
先赋值给
d
p
[
i
−
1
]
。然后讨论配合前面一个字符能否解码
i
≥
1
dp[i] =
初始化从0开始,然后循环往后即可。
(3) 初始状态按照状态转移方程来设置即可。
(4) 执行状态转移则是用循环执行i ≥ 1 \geq 1 ≥1的部分。
(5) 返回最终的解就是返回dp[n-1]。
给定一个整数数组,找出总和最大的连续数列,并返回总和。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
int dp[n+5];
memset(dp, 0, sizeof(dp));
int max0 = INT_MIN;
for(int i = 0; i < n; ++i){
if(i == 0){
dp[i] = nums[0];
} else{
dp[i] = max(nums[i], dp[i-1] + nums[i]);
}
max0 = max(dp[i], max0);
}
return max0;
}
};
(1) 相对基础的一维DP问题,dp[i]表示为选取nums[i]的最大连续数列为多少。
(2) 状态转移方程为
d
p
[
i
]
=
{
n
u
m
s
[
0
]
,
i
=
0
m
a
x
(
n
u
m
s
[
i
]
,
d
p
[
i
−
1
]
+
n
u
m
s
[
i
]
)
,
i
≥
1
dp[i] =
(3) 初始状态按照状态转移方程来设置即可
(4) 执行状态转移方程一层循环即可。
(5) 最终解是dp[i]的最大值。
本篇文章通过十道例题,带领读者理解一维DP的一些比较基础的问题,读者可以根据自身实际情况,选取LeetCode上的其他情况进行练习。