题目一、爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
C++核心代码
class Solution {
public:
int climbStairs(int n) {
if(n<=1) return 1;
std::vector<int> dp(n+1,0);
dp[0]=1;
dp[1]=1;
for(int i =2;i<=n;++i){
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n];
}
};
思路分析
-
问题描述:
- 你要爬到第
n
阶楼梯,每次可以爬 1 个或 2 个台阶。需要计算有多少种不同的方法可以到达第n
阶楼梯。
- 你要爬到第
-
动态规划思路:
- 定义状态:
- 设
dp[i]
为到达第i
阶楼梯的方法数。
- 设
- 状态转移方程:
- 从第
i
阶楼梯可以由第i-1
阶楼梯或第i-2
阶楼梯到达,因此dp[i] = dp[i-1] + dp[i-2]
。
- 从第
- 初始化:
dp[0]
和dp[1]
是边界条件,分别为 1。即到达第 0 阶和第 1 阶楼梯的方式数都是 1。
- 计算:
- 从第 2 阶楼梯开始,根据状态转移方程逐步计算每一阶的方法数,直到第
n
阶。
- 从第 2 阶楼梯开始,根据状态转移方程逐步计算每一阶的方法数,直到第
- 定义状态:
题目二、打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
C++核心代码
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 0) return 0;
if(n == 1) return nums[0];
std::vector<int>dp(n,0);
dp[0] = nums[0];
dp[1] = std::max(nums[0],nums[1]);
for(int i =2;i<n;++i){
dp[i]=std::max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[n-1];
}
};
动态规划解决方案
-
定义状态:
dp[i]
表示偷窃到第i
间房屋时的最大金额。
-
状态转移方程:
- 如果偷窃第
i
间房屋,前一间房屋i-1
不能偷,因此:dp[i] = dp[i-2] + nums[i]
- 如果不偷窃第
i
间房屋,最大金额就是前一间房屋的最大金额:dp[i] = dp[i-1]
- 综合起来,
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
- 如果偷窃第
-
初始化:
dp[0] = nums[0]
:如果只有一个房屋,只能偷窃这一间房屋。dp[1] = max(nums[0], nums[1])
:如果有两个房屋,选择金额较高的一个。
-
计算结果:
- 通过迭代更新
dp
数组中的值,最终dp[n-1]
就是所求的最大金额。
- 通过迭代更新
题目三、单词拆分
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
C++核心代码
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordset(wordDict.begin(),wordDict.end());
vector<bool> dp(s.length() + 1, false);
dp[0] = true;
for (int i = 1; i <= s.length(); ++i) {
for (int j = 0; j < i; ++j) {
if (dp[j] && wordset.find(s.substr(j, i - j)) != wordset.end()) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
};
解释
-
初始化:
unordered_set
用于快速查找字典中的单词。dp
数组初始化,dp[0]
为true
,其他为false
。 -
填充
dp
数组:- 外层循环
i
遍历到字符串的每一个位置。 - 内层循环
j
遍历从0
到i
的每一个位置,检查dp[j]
是否为true
并且从j
到i
的子串是否在字典中存在。 - 如果找到有效的子串,则将
dp[i]
设置为true
并跳出内层循环。
- 外层循环
-
返回结果: 最终
dp[s.length()]
表示整个字符串s
是否可以由字典中的单词拼接而成。
例子解释
对于 s = "leetcode"
和 wordDict = ["leet", "code"]
:
-
wordSet
初始化为{ "leet", "code" }
。 -
dp
数组初始化为{ true, false, false, false, false, false, false, false, false }
。
-
当
i = 4
:j = 0
:s[0:4]
是"leet"
。dp[0]
是true
,"leet"
在字典中,所以dp[4]
设置为true
,并跳出内层循环。
-
当
i = 8
:j = 0
:s[0:8]
是"leetcode"
。dp[0]
是true
,但"leetcode"
不在字典中。j = 1
:s[1:8]
是"eetcode"
。dp[1]
是false
。j = 2
:s[2:8]
是"etcode"
。dp[2]
是false
。j = 3
:s[3:8]
是"tcode"
。dp[3]
是false
。j = 4
:s[4:8]
是"code"
。dp[4]
是true
,"code"
在字典中,所以dp[8]
设置为true
,并跳出内层循环。
题目四、最长递增子序列
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的
子序列。
C++核心代码
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.empty()) return 0;
vector<int> dp(nums.size(),1);
for(int i =1;i<nums.size();++i){
for(int j = 0;j<i;++j){
if(nums[i]>nums[j]){
dp[i] = max(dp[i],dp[j]+1);
}
}
}
return *max_element(dp.begin(), dp.end());
}
};
动态规划算法
1. 初始化
- 数组
dp
:用于存储以每个元素结尾的最长递增子序列的长度。初始化为[1, 1, 1, 1, 1, 1, 1, 1]
,因为每个元素自身可以形成一个长度为1
的递增子序列。
2. 外层循环
- 外层循环遍历每个元素
nums[i]
从i = 1
到7
(因为数组长度是8
),来更新dp
数组中的值。
3. 内层循环
- 内层循环遍历
j
从0
到i - 1
,用于检查nums[i]
是否可以延续nums[j]
的递增子序列。
详细步骤
数组状态:
- 初始:
nums = [10, 9, 2, 5, 3, 7, 101, 18]
- 初始
dp
数组:dp = [1, 1, 1, 1, 1, 1, 1, 1]
1. 处理 i = 1
(nums[1] = 9
)
-
内层循环
j = 0
:nums[1] = 9
和nums[0] = 10
。nums[1] < nums[0]
,不更新dp[1]
。
dp
数组未变:[1, 1, 1, 1, 1, 1, 1, 1]
2. 处理 i = 2
(nums[2] = 2
)
-
内层循环:
j = 0
:nums[2] = 2
和nums[0] = 10
,2 < 10
,不更新dp[2]
。j = 1
:nums[2] = 2
和nums[1] = 9
,2 < 9
,不更新dp[2]
。
dp
数组未变:[1, 1, 1, 1, 1, 1, 1, 1]
3. 处理 i = 3
(nums[3] = 5
)
-
内层循环:
j = 0
:nums[3] = 5
和nums[0] = 10
,5 < 10
,不更新dp[3]
。j = 1
:nums[3] = 5
和nums[1] = 9
,5 < 9
,不更新dp[3]
。j = 2
:nums[3] = 5
和nums[2] = 2
,5 > 2
,更新dp[3] = max(dp[3], dp[2] + 1) = max(1, 2) = 2
。
dp
数组更新:[1, 1, 1, 2, 1, 1, 1, 1]
4. 处理 i = 4
(nums[4] = 3
)
-
内层循环:
j = 0
:nums[4] = 3
和nums[0] = 10
,3 < 10
,不更新dp[4]
。j = 1
:nums[4] = 3
和nums[1] = 9
,3 < 9
,不更新dp[4]
。j = 2
:nums[4] = 3
和nums[2] = 2
,3 > 2
,更新dp[4] = max(dp[4], dp[2] + 1) = max(1, 2) = 2
。
dp
数组更新:[1, 1, 1, 2, 2, 1, 1, 1]
5. 处理 i = 5
(nums[5] = 7
)
-
内层循环:
j = 0
:nums[5] = 7
和nums[0] = 10
,7 < 10
,不更新dp[5]
。j = 1
:nums[5] = 7
和nums[1] = 9
,7 < 9
,不更新dp[5]
。j = 2
:nums[5] = 7
和nums[2] = 2
,7 > 2
,更新dp[5] = max(dp[5], dp[2] + 1) = max(1, 2) = 2
。j = 3
:nums[5] = 7
和nums[3] = 5
,7 > 5
,更新dp[5] = max(dp[5], dp[3] + 1) = max(2, 3) = 3
。
dp
数组更新:[1, 1, 1, 2, 2, 3, 1, 1]
6. 处理 i = 6
(nums[6] = 101
)
-
内层循环:
j = 0
:nums[6] = 101
和nums[0] = 10
,101 > 10
,更新dp[6] = max(dp[6], dp[0] + 1) = max(1, 2) = 2
。j = 1
:nums[6] = 101
和nums[1] = 9
,101 > 9
,更新dp[6] = max(dp[6], dp[1] + 1) = max(2, 2) = 2
。j = 2
:nums[6] = 101
和nums[2] = 2
,101 > 2
,更新dp[6] = max(dp[6], dp[2] + 1) = max(2, 2) = 2
。j = 3
:nums[6] = 101
和nums[3] = 5
,101 > 5
,更新dp[6] = max(dp[6], dp[3] + 1) = max(2, 3) = 3
。j = 4
:nums[6] = 101
和nums[4] = 3
,101 > 3
,更新dp[6] = max(dp[6], dp[4] + 1) = max(3, 3) = 3
。j = 5
:nums[6] = 101
和nums[5] = 7
,101 > 7
,更新dp[6] = max(dp[6], dp[5] + 1) = max(3, 4) = 4
。
dp
数组更新:[1, 1, 1, 2, 2, 3, 4, 1]
7. 处理 i = 7
(nums[7] = 18
)
-
内层循环:
j = 0
:nums[7] = 18
和nums[0] = 10
,18 > 10
,更新dp[7] = max(dp[7], dp[0] + 1) = max(1, 2) = 2
。j = 1
:nums[7] = 18
和nums[1] = 9
,18 > 9
,更新dp[7] = max(dp[7], dp[1] + 1) = max(2, 2) = 2
。j = 2
:nums[7] = 18
和nums[2] = 2
,18 > 2
,更新dp[7] = max(dp[7], dp[2] + 1) = max(2, 2) = 2
。j = 3
:nums[7] = 18
和nums[3] = 5
,18 > 5
,更新dp[7] = max(dp[7], dp[3] + 1) = max(2, 3) = 3
。j = 4
:nums[7] = 18
和nums[4] = 3
,18 > 3
,更新dp[7] = max(dp[7], dp[4] + 1) = max(3, 3) = 3
。j = 5
:nums[7] = 18
和nums[5] = 7
,18 > 7
,更新dp[7] = max(dp[7], dp[5] + 1) = max(3, 4) = 4
。j = 6
:nums[7] = 18
和nums[6] = 101
,18 < 101
,不更新dp[7]
。
dp
数组更新:[1, 1, 1, 2, 2, 3, 4, 4]
结果
- 最长递增子序列的长度:
*max_element(dp.begin(), dp.end())
返回4
。
对于数组 nums = [10, 9, 2, 5, 3, 7, 101, 18]
,最长严格递增子序列的长度是 4
。一个可能的最长递增子序列是 [2, 3, 7, 101]
或 [2, 5, 7, 18]
。