主要供自己回顾学习,会持续更新,题源codetop动态规划+近半年
1.零钱兑换
给你 k 种面值的硬币,面值分别为 c1, c2 … ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。
dp[n]的意义为凑成总金额n,最少需要dp[n]枚硬币
amount怎么来的?撤回一个硬币+1来的,dp[i] = min(dp[i], dp[i - coin] + 1),其中是coin的原因是,到底撤回面值多少的硬币,还需要穷举
另外,要取min所以dp应该初始化为一个较大值,不能是INT_MAX,因为递推式是dp+1,会溢出
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, amount + 1);
dp[0] = 0;
for(int i = 0; i <= amount; i ++){
for(int coin : coins){
if(i - coin < 0) continue;
dp[i] = min(dp[i], dp[i - coin] + 1);
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
};
还有一种视作完全背包的写法,一样的初始化和递推,将硬币视为物品,面值视为重量和价值,背包容量是amount
其中dp[j - coins[i]] != INT_MAX必不可少,因为如果其为INT_MAX的话,后序计算dp会+1,就会溢出
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for(int i = 0; i < coins.size(); i ++){//遍历物品
for(int j = coins[i]; j <= amount; j ++){//遍历背包
if(dp[j - coins[i]] != INT_MAX) dp[j] = min(dp[j], dp[j - coins[i]] + 1);
}
}
if(dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
2.零钱兑换II
和零钱兑换一样,不一样的是这里要求返回硬币组合数。
可以视作完全背包做
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
这个题求的是组合,所以外层遍历背包,内存遍历物品。求组合数量的递推公式:dp[j]+=dp[j - coins[i]]
从这个递推式来看,初始化一定要有不为0的
dp[0] = 1,其他初始化为0
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1, 0);
dp[0] = 1;
for(int i = 0; i < coins.size(); i ++){
for(int j = coins[i]; j <= amount; j ++){
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
};
3.面试题08.11.硬币
给定数量不限的硬币,币值为25分、10分、5分和1分,编写代码计算n分有几种表示法。(结果可能会很大,你需要将结果模上1000000007)
dp[i]的含义是i分有几种表示方法,由于5,1和1,5不用特别区分,所以求的是组合数,外层遍历物品,内层遍历背包容量
当前硬币有取或者不取,取得话就是dp[i - coin],不
class Solution {
public:
const int mod = 1e9 + 7;
int waysToChange(int n) {
vector<int> dp(n + 1, 0);
vector<int> coins = {1, 5, 10, 25};
dp[0] = 1;
for (int coin : coins) {
for (int i = coin; i <= n; i++) {
dp[i] = (dp[i] + dp[i - coin]) % mod;
}
}
return dp[n];
}
};
4.单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
单词就是物品,字符串就是背包,因为物品可以重复填背包,所以这是个完全背包问题!
dp[i]:表示下标为0~i - 1的字符串可以被拆分为一个或多个在字典中出现的单词。
如果dp[j] == true且[j,i)出现在字典中了,那么dp[i] 也为 true,这句话记住基本就做出来了
本题求的是排列数,因为applepen是apple+pen的组合而不是pen+apple的组合
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
vector<bool> dp(s.size(), false);
unordered_set<string> word(wordDict.begin(), wordDict.end());
dp[0] = true;
for(int i = 1; i <= s.size(); i ++){
for(int j = 0; j < i; j ++){
if(dp[j] && word.find(s.substr(j, i - j)) != word.end()) dp[i] = true;
}
}
return dp[s.size()];
}
};
5.最长递增子序列
输入一个无序的整数数组,请你找到其中最长的严格递增子序列的长度。
注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的。
dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度
那么base case就是dp[0] = 1
以下是完整代码,其中res初始化为 1 至关重要。
如果 res 初始化为 0,而数组是递减的,那么 res 将永远保持 0,因为没有任何 dp[j] 会大于 1,所以内部循环的条件 if(res < dp[j]) 永远不会满足。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
//dp[i]是到nums[i]截止的
vector<int> dp(nums.size() + 1, 1);
int result = 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);
if(result < dp[i]) result = dp[i];
}
}
return result;
}
};
6.最长递增子序列的个数
给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。
注意 这个数列必须是 严格 递增的。
dp[i]:最长递增序列的长度
cnt[i]:最长递增序列的个数
设nums的最长上升子序列的长度为maxLen,那么答案为所有满足dp[i] = maxLen的 i 对应的cnt[i]之和
我们从小到大计算 dp数组的值,在计算 dp[i]之前,我们已经计算出 dp[0…i−1]的值,则状态转移方程为:
dp[i]=max(dp[j])+1,其中 0≤j<i 且 num[j]<num[i]
即考虑往 dp[0…i−1]中最长的上升子序列后面再加一个 nums[i]。
由于 dp[j] 代表 nums[0…j] 中以 nums[j] 结尾的最长上升子序列,所以如果能从 dp[j] 这个状态转移过来,那么 nums[i] 必然要大于 nums[j],才能将 nums[i] 放在 nums[j] 后面以形成更长的上升子序列。
对于 cnt[i],其等于所有满足 dp[j] + 1 = dp[i] 的 cnt[j]之和。在代码实现时,我们可以在计算 dp[i]的同时统计 cnt[i]的值。
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n), cnt(n);
int ans = 0, maxLen = 0;
for(int i = 0; i < n; ++i) {
dp[i] = 1; // 初始化为1
cnt[i] = 1; // 初始化为1
for(int j = 0; j < i; ++j) {
if(nums[i] > nums[j]) {
if(dp[j] + 1 > dp[i]) { //相当于 dp[i] = max(dp[j]+1, dp[i]);
dp[i] = dp[j] + 1;
cnt[i] = cnt[j];
}else if(dp[j] + 1 == dp[i]){ //dp[j]+1 == dp[i] 有相同的长度
cnt[i] += cnt[j];
}
}
}
if(dp[i] > maxLen) {
maxLen = dp[i];
ans = cnt[i];
} else if(dp[i] == maxLen) {
ans += cnt[i];
}
}
cout << ans << endl;
return ans;
}
};
7.得到山形数组的最少删除次数
我们定义 arr 是 山形数组 当且仅当它满足:
- arr.length >= 3
- 存在某个下标 i (从 0 开始) 满足 0 < i < arr.length - 1 且:
arr[0] < arr[1] < … < arr[i - 1] < arr[i]
arr[i] > arr[i + 1] > … > arr[arr.length - 1]
给你整数数组 nums ,请你返回将 nums 变成 山形状数组 的 最少 删除次数。
可以两部分处理这个问题:
- 寻找所有可能的递增序列的最长长度。
- 寻找所有可能的递减序列的最长长度。
1可以使用类似于最长递增子序列(LIS)的算法来实现。
2可以使用类似于最长递减子序列(LDS)的算法来实现。
对于每个位置 i,可以计算它作为山峰的情况下的最长山形子序列的长度,并求取最大的长度。然后,从总长度中减去这个最大长度就是最少删除次数。
class Solution {
public:
int minimumMountainRemovals(vector<int>& nums) {
int n = nums.size();
if(n < 3) return 0;
//记录上升子序列
vector<int> dp(n, 1);
for(int j = 1; j < n; j ++){
for(int i = 0; i < j; i ++){
if(nums[j] > nums[i]) dp[j] = max(dp[j], dp[i] + 1);
}
}
//记录下降子序列
vector<int> dp2(n, 1);
for(int j = n - 2; j >= 0; j --){
for(int i = n - 1; i > j; i --){
if(nums[j] > nums[i]) dp2[j] = max(dp2[j], dp2[i] + 1);
}
}
//找到了最长的山行子序列的长度
int maxLen = 0;
//以中心为基准
for(int i = 1; i < n - 1; i ++){
if(dp[i] > 1 && dp2[i] > 1) maxLen = max(maxLen, dp[i] + dp2[i] - 1);
}
return n - maxLen;
}
};
8.最长公共子序列
text1和text2,返回最长处公共子序列的长度
dp[i][j]表示text1下标0~i-1和text2下标0 ~ j-1最长公共子序列的长度
所以dp的长度需要是len+1
text1[i - 1] == text2[j - 1],dp[i][j] = dp[i - 1][j - 1] + 1
text1[i - 1] != text2[j - 1],dp[i][j] = max(dp[i][j - 1],dp[i - 1][j])
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int len1 = text1.size();
int len2 = text2.size();
vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
int res = 0;
for(int i = 1; i <= len1; i ++){
for(int j = 1; j <= len2; j ++){
if(text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
if(res < dp[i][j]) res = dp[i][j];
}
}
return res;
}
};
9.最长重复子数组
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。
dp[i][j] 的含义是,nums1以下标i - 1结尾的子数组和nums2以下标j - 1结尾的子数组重复的最长子数组长度
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
int len1 = nums1.size();
int len2 = nums2.size();
int result = 0;
vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
for(int i = 1; i <= len1; i ++){
for(int j = 1; j <= len2; j ++){
if(nums1[i - 1] == nums2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
if(result < dp[i][j]) result = dp[i][j];
}
}
return result;
}
};
10.最长等差数列
给你一个整数数组 nums,返回 nums 中最长等差子序列的长度。
dp[i][j]:以nums[i]结尾的子序列中,差值是j的最长等差数列长度
nums[i] - nums[j] = diff
dp[i][diff] = dp[j][diff] + 1
res实时变化
另外,由于0 <= nums[i] <= 500,所以diff在-500到500之间,又由于索引不可为负数,所以diff应该加500,变成0到1000间
class Solution {
public:
int longestArithSeqLength(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> dp(n, vector<int>(1001, 1));
int res = dp[0][0];
for(int i = 0; i < n; i ++){
for(int j = 0; j < i; j ++){
int diff = nums[i] - nums[j] + 500;
dp[i][diff] = dp[j][diff] + 1;
res = max(res, dp[i][diff]);
}
}
return res;
}
};