打家劫舍问题
198. 打家劫舍
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
class Solution {
public:
int rob(vector<int>& nums) {
int len = nums.size();
if(len == 0) return 0;
int pre = 0, cur = 0;
for(auto &i : nums){
int temp = cur;
cur = max(pre + i, cur);
pre = temp;
}
return cur;
}
};
213. 打家劫舍 II (围成一圈)
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻
的。
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 1) return nums[0];
return max(robrange(nums, 0, n-2), robrange(nums, 1, n-1));
}
int robrange(vector<int>& nums, int start, int end) {
int pre = 0, cur = 0;
for(int i=start; i<=end; ++i){
int temp = cur;
cur = max(pre + nums[i], cur);
pre = temp;
}
return cur;
}
};
337. 打家劫舍 III
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
输入: [3,2,3,null,3,null,1]
3
/
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
要加memo,否则会超时
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
unordered_map<TreeNode*, int> memo;
int rob(TreeNode* root) {
//抢和不抢
if(root==nullptr) return 0;
if(memo.find(root)!=memo.end()) return memo[root];
int do_it = root->val +
(root->left==nullptr?0:rob(root->left->left) + rob(root->left->right)) +
(root->right==nullptr?0:rob(root->right->left) + rob(root->right->right));
int not_do = rob(root->left) + rob(root->right);
int res = max(do_it, not_do);
memo[root] = res;
return res;
}
};
买卖股票问题
121. 买卖股票的最佳时机(买卖一次)
状态为今天持不持股
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0.
class Solution {
public:
int maxProfit(vector<int>& prices) {
/*
dp[i][0]:规定了今天不持股,有以下两种情况:
昨天不持股,今天什么都不做;
昨天持股,今天卖出股票(现金数增加),
dp[i][1]:规定了今天持股,有以下两种情况:
昨天持股,今天什么都不做(现金数与昨天一样);
昨天不持股,今天买入股票(注意:只允许交易一次,因此手上的现金数就是当天的股价的相反数)。
*/
int len = prices.size();
vector<vector<int> > dp(len, vector<int>(2));
dp[0][0] = 0; //不持股
dp[0][1] = -prices[0]; //持股
for(int i=1; i<len; ++i){
dp[i][0] = max(dp[i-1][0], prices[i]+dp[i-1][1]);
dp[i][1] = max(dp[i-1][1], -prices[i]);
}
return dp[len-1][0];
}
};
122. 买卖股票的最佳时机 II(买卖多次)
给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
class Solution {
public:
int maxProfit(vector<int>& prices) {
/*
dp[i][0]:规定了今天不持股,有以下两种情况:
昨天不持股,今天什么都不做;
昨天持股,今天卖出股票(现金数增加),
dp[i][1]:规定了今天持股,有以下两种情况:
昨天持股,今天什么都不做(现金数与昨天一样);
昨天不持股,今天买入股票。
*/
int len = prices.size();
vector<vector<int> > dp(len, vector<int>(2));
dp[0][0] = 0; //不持股
dp[0][1] = -prices[0]; //持股
for(int i=1; i<len; ++i){
dp[i][0] = max(dp[i-1][0], prices[i]+dp[i-1][1]);
dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]);
}
return dp[len-1][0];
}
};
123. 买卖股票的最佳时机 III(最多两笔)
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成两笔
交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
class Solution {
public:
int maxProfit(vector<int>& prices) {
// 需要把已经交易了多少次设置成为一个状态的维度
int len = prices.size();
int dp[len][3][2];
memset(dp, 0, sizeof(dp));
dp[0][1][1] = -prices[0];
dp[0][2][1] = INT32_MIN;
for(int i=1; i<len; ++i){
dp[i][1][0] = max(dp[i-1][1][0], prices[i]+dp[i-1][1][1]);
dp[i][1][1] = max(dp[i-1][1][1], -prices[i]);
dp[i][2][0] = max(dp[i-1][2][0], prices[i]+dp[i-1][2][1]);
dp[i][2][1] = max(dp[i-1][2][1], -prices[i]+dp[i-1][1][0]);
}
return max(dp[len-1][2][0], dp[len-1][1][0]);
}
};
188. 买卖股票的最佳时机 IV(最多k笔)
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成
k 笔
交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int len = prices.size();
int dp[k+1][2];
memset(dp, 0, sizeof(dp));
for (int i = 0; i <= k; i++) {
dp[i][1] = INT32_MIN; //注意初始化
//dp[0][1]:表示一次交易都没有发生,但是持股,这是不可能的,也不会有后序的决策要用到这个状态值,可以不用管;
//dp[x][1] 可能是负值,如果我们简单的初始化为0,会干扰我们的结果,必须初始化最小值比较好
}
for (int price : prices) {
for (int j = 1; j <= k; j++) {
dp[j][1] = max(dp[j][1], dp[j - 1][0] - price);
dp[j][0] = max(dp[j][0], dp[j][1] + price);
}
}
return dp[k][0];
}
};
309. 最佳买卖股票时机含冷冻期(买卖多次+冷冻期1一天)
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
int dp[len][3];
memset(dp, 0, sizeof(dp));
dp[0][1] = -prices[0];
// dp[i][0]: 手上不持有股票,并且今天不是由于卖出股票而不持股,我们拥有的现金数
// dp[i][1]: 手上持有股票时,我们拥有的现金数
// dp[i][2]: 手上不持有股票,并且今天是由于卖出股票而不持股,我们拥有的现金数
for (int i = 1; i < len; i++) {
dp[i][0] = max(dp[i - 1][0], dp[i - 1][2]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
dp[i][2] = dp[i - 1][1] + prices[i];
}
return max(dp[len - 1][0], dp[len - 1][2]);
}
};
714. 买卖股票的最佳时机含手续费(买卖多次+手续费)
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
//相比于 买卖多次 在转移方程加fee即可
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int len = prices.size();
vector<vector<int> > dp(len, vector<int>(2));
dp[0][0] = 0; //不持股
dp[0][1] = -prices[0]; //持股
for(int i=1; i<len; ++i){
dp[i][0] = max(dp[i-1][0], prices[i]+dp[i-1][1]-fee);
dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]);
}
return dp[len-1][0];
}
};
背包问题
通用解法
01 背包问题
:
如果是 01 背包,即数组中的元素不可重复使用,外循环遍历 arrs,内循环遍历 target,且内循环倒序(因为物品只能用一次,从二维压缩到一维后 不能让之前的结果影响后来的结果所以要倒序):
完全背包问题
:
(1)如果是完全背包,即数组中的元素可重复使用并且不考虑元素之间顺序,arrs 放在外循环,target在内循环,且内循环正序。(因为可以用多次,所以不用倒序)
(2)如果组合问题需考虑元素之间的顺序,需将 target 放在外循环,将 arrs 放在内循环,且内循环正序。
01背包
416. 分割等和子集
给你一个只包含正整数的非空数组nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
class Solution {
public:
bool canPartition(vector<int>& nums) {
if(nums.size() < 2) return false;
//计算总和
int sum = accumulate(nums.begin(), nums.end(), 0.0);
int maxNum = *max_element(nums.begin(), nums.end());
if(sum % 2) return false;
int target = sum / 2;
if (maxNum > target) return false; //[99,1]
vector<vector<bool> > dp(nums.size(), vector<bool>(target+1));
for (int i = 0; i < nums.size(); i++) {
dp[i][0] = true; //如果不选取任何正整数,则被选取的正整数等于 0
}
dp[0][nums[0]] = true; //只有一个正整数 nums[0] 可以被选取
for(int i=1; i<nums.size(); ++i){
for(int j=1; j<=target; ++j){
if(j-nums[i]<0){ //和不足,不能取第i个物品
dp[i][j] = dp[i-1][j];
}
else{
dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i]]; //取不取第i个物品
}
}
}
return dp[nums.size()-1][target];
/*
优化:
// vector<int> dp(target + 1, 0);
dp[0] = true;
for (int i = 0; i < n; i++) {
int num = nums[i];
for (int j = target; j >= num; --j) {
dp[j] |= dp[j - num];
}
}
return dp[target];
*/
}
};
494. 目标和
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
/*
加正负符号得到最终的目标和,相当于我们把加+的集合在一起就是正数和x,把加-的集合在一起就是负数和y.而正数和x就是我们背包问题的目标和。
x-y = target 而x+y = nums的和sum 因此x=(target+sum)/2.
也就是从nums里选出几个数,使其和为x,这就转换成了0-1背包问题
*/
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for (int& num : nums) {
sum += num;
}
int diff = sum - target;
if (diff < 0 || diff % 2 != 0) {
return 0;
}
int neg = diff / 2;
vector<int> dp(neg + 1);
dp[0] = 1;
for (int& num : nums) {
for (int j = neg; j >= num; j--) {
dp[j] += dp[j - num];
}
}
return dp[neg];
}
};
1049 最后一块石头的重量 II
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
/*
和494类似,加正负符号。记负号石头之和是neg,正号之和就是sum-neg
(sum−neg)−neg=sum−2⋅neg
要使最后一块石头的重量尽可能地小,neg 需要在不超过 ⌊sum/2⌋ 的前提下尽可能地大
因此本问题可以看作是背包容量为⌊sum/2⌋,物品重量和价值均为stones_i
*/
class Solution {
public:
int lastStoneWeightII(vector<int> &stones) {
int sum = accumulate(stones.begin(), stones.end(), 0); //#include <numeric>
int n = stones.size(), m = sum / 2;
vector<vector<int> > dp(n + 1, vector<int>(m + 1));
dp[0][0] = true; //其中 dp[i+1][j] 表示前i个石头能否凑出重量 j。
for (int i = 0; i < n; ++i) {
for (int j = 0; j <= m; ++j) {
if (j < stones[i]) {
dp[i + 1][j] = dp[i][j];
} else {
dp[i + 1][j] = dp[i][j] || dp[i][j - stones[i]];
}
}
}
for (int j = m;; --j) {
if (dp[n][j]) {
return sum - 2 * j;
}
}
}
};
完全背包-组合
518. 零钱兑换 II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。
示例 3:
输入: amount = 10, coins = [10]
输出: 1
//如果组合问题需考虑元素之间的顺序,需将 target 放在外循环,将 arrs 放在内循环,且内循环正序
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1); //dp[i] 目标金额为i时的组合数
dp[0] = 1;
for (int& coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
};
完全背包-排列
322. 零钱兑换II
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT16_MAX); //dp[i] 目标金额为i时的最小硬币数
dp[0] = 0;
//遍历所有状态的取值
for (int i = 1; i <= amount; i++) {
for(int &coin : coins){
// 子问题无解,跳过
if(i-coin<0){
continue;
}
dp[i] = min(dp[i],dp[i-coin]+1);
}
}
return dp[amount] == INT16_MAX? -1 : dp[amount];
}
};
139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 2:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
示例 3:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
//使用worddict能否构成s, 可用多次, 完全背包 + 排列
int num = wordDict.size();
int len_s = s.size();
vector<bool> dp(len_s + 1); //dp[i] 为从0-i的单词是否能由wordDict中的单词拼接而来
dp[0] = true; //不使用任何构成字符串
for (int i = 1; i <= len_s; i++) {
for(int &word : wordDict){
int wl = word.size();
if(i-wl >= 0 and word == s.substr(i-wl, wl)){
dp[i] = dp[i] or dp[i-wl];
}
}
}
return dp[len_s];
}
};
983. 最低票价
在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 days 的数组给出。每一项是一个从 1 到 365 的整数。
火车票有三种不同的销售方式:
一张为期一天的通行证售价为 costs[0] 美元;
一张为期七天的通行证售价为 costs[1] 美元;
一张为期三十天的通行证售价为 costs[2] 美元。
通行证允许数天无限制的旅行。 例如,如果我们在第 2 天获得一张为期 7 天的通行证,那么我们可以连着旅行 7 天:第 2 天、第 3 天、第 4 天、第 5 天、第 6 天、第 7 天和第 8 天。
返回你想要完成在给定的列表 days 中列出的每一天的旅行所需要的最低消费。
示例 1:
输入:days = [1,4,6,7,8,20], costs = [2,7,15]
输出:11
解释:
例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划:
在第 1 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 1 天生效。
在第 3 天,你花了 costs[1] = $7 买了一张为期 7 天的通行证,它将在第 3, 4, …, 9 天生效。
在第 20 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 20 天生效。
你总共花了 $11,并完成了你计划的每一天旅行。
示例 2:
输入:days = [1,2,3,4,5,6,7,8,9,10,30,31], costs = [2,7,15]
输出:17
解释:
例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划:
在第 1 天,你花了 costs[2] = $15 买了一张为期 30 天的通行证,它将在第 1, 2, …, 30 天生效。
在第 31 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 31 天生效。
你总共花了 $17,并完成了你计划的每一天旅行。
class Solution {
public:
int mincostTickets(vector<int>& days, vector<int>& costs) {
int last = days[days.size()-1];
vector<int> dp(last+1, 0); //当前某一天需要花费的最少费用
int idx = 0;
for (int i = 1; i <= last; i++) {
if (i == days[idx]) {
int cost = INT_MAX;
int oneDayAgo = i-1;
int sevenDaysAgo = i-7>0?i-7:0;
int thirtyDaysAgo = i-30>0?i-30:0;
cost = min({dp[oneDayAgo] + costs[0], dp[sevenDaysAgo] + costs[1], dp[thirtyDaysAgo] + costs[2], cost});
dp[i] = cost;
idx++;
} else {
dp[i] = dp[i-1]; //贪心:当前不是待处理天数
}
}
return dp[last];
}
};
279. 完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
class Solution {
public:
int numSquares(int n) {
vector<int> f(n + 1, INT_MAX); //和为n时的个数
f[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j * j <= i; j++) {
f[i] = min(f[i], f[i - j * j]+1);
}
}
return f[n];
}
};
377. 组合总和 Ⅳ
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<unsigned int> dp(target+1); //和为i时的组合数
//16位系统中一个int能存储的数据的范围为-32768~32767,而unsigned int能存储的数据范围则是0~65535
dp[0] = 1;
for(int i=1; i<=target; ++i){
for(auto &num : nums){
if(i >= num){
dp[i] = dp[i] + dp[i-num];
}
}
}
return dp[target];
}
};
最长子序列
300. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。
例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int len = nums.size();
vector<int> dp(len); // dp[i] 为 到i的最长序列长度
int res = INT32_MIN;
for(int i=0; i<len; ++i){
dp[i] = 1;
for(int j=0; j<i; ++j){
if(nums[j] < nums[i]){
dp[i] = max(dp[i], dp[j]+1);
}
}
res = max(res, dp[i]);
}
return res;
}
};
673. 最长递增子序列的个数
给定一个未排序的整数数组,找到最长递增子序列的个数。
示例 1:
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int len = nums.size();
vector<int> dp(len, 1); // dp[i] 为 到i的最长序列长度
vector<int> count(len, 1); // count[i] 为 到i的最长序列长度个数
int max_length = 0;
for(int i=0; i<len; ++i){
for(int j=0; j<i; ++j){
if(nums[j] < nums[i]){
if(dp[i] < dp[j]+1){
dp[i] = dp[j] + 1;
count[i] = count[j];
}
else if(dp[i] == dp[j]+1){
count[i] += count[j];
}
}
}
max_length = max(max_length, dp[i]);
}
int res = 0;
for(int i=0; i<len; ++i){
if(dp[i] == max_length){
res += count[i];
}
}
return res;
}
};
674. 最长连续递增序列
给定一个未经排序的整数数组,找到最长且连续递增的子序列,并返回该序列的长度。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int len = nums.size();
vector<int> dp(len, 1); // dp[i] 为 到i的最长序列长度
int res = 0;
for(int i=1; i<len; ++i){
if(nums[i-1] < nums[i]){
dp[i] = dp[i-1]+1;
}
res = max(res, dp[i]);
}
return res;
}
};
72.编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
状态:dp[i+1][j+1] 为s1[0,…,i]和s2[0,…,j]之间的编辑距离
s1[i-1]!=s2[j-1]时,有三种操作:
- 增:在 s1[i-1] 插入一个和 s2[j-1] 一样的字符 dp[i][j-1]
- 删:s[i-1] 这个字符删掉 dp[i-1][j]
- 替:直接把 s1[i-1] 替换成 s2[j-1] dp[i-1][j-1]
class Solution {
public:
int minDistance(string word1, string word2) {
int lenA = word1.size();
int lenB = word2.size();
if(lenA * lenB == 0) return lenA+lenB;
int dp[lenA+1][lenB+1];
for(int i=0; i<lenA+1; ++i){
dp[i][0] = i;
}
for(int j=0; j<lenB+1; ++j){
dp[0][j] = j;
}
for(int i=1; i<lenA+1; ++i){
for(int j=1; j<lenB+1; ++j){
if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
else{
dp[i][j] = min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + 1;
}
}
}
return dp[lenA][lenB];
}
};
相似题目:
583. 两个字符串的删除操作
和上题相比只有删除操作,只需要改不相等时,把s1删除掉或者把s2删除掉。
712. 两个字符串的最小ASCII删除和
比较的时候加上ASCII码值即可。
1035. 不相交的线
每个数字都可以选择连或者不连,当两值相等的时候,必然是dp[i-1][j-1]+1;不等的时候,选择连的话,可以是dp[i][j-1],选择不连的话,可以是dp[i-1][j-1]和dp[i-1][j]。
1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size(),n = text2.size();
int dp[m+1][n+1];
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if(text1[i-1]==text2[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
} else{
dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[m][n];
}
};
516. 最长回文子序列
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1:
输入:
“bbbab”
输出:
4
一个可能的最长回文子序列为 “bbbb”。
示例 2:
输入:
“cbbd”
输出:
2
一个可能的最长回文子序列为 “bb”。
//斜着便遍历或者反着遍历
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.size();
int dp[n][n];
memset(dp, 0, sizeof(dp));
for (int i = 0; i < n; i++) dp[i][i] = 1;
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
if (s[i] == s[j])
dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
return dp[0][n - 1];
}
};
5.最长回文子串
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if (n < 2) {
return s;
}
int maxLen = 1;
int begin = 0;
// dp[i][j] 表示 s[i..j] 是否是回文串
vector<vector<int>> dp(n, vector<int>(n));
// 初始化:所有长度为 1 的子串都是回文串
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 递推开始
// 先枚举子串长度
for (int L = 2; L <= n; L++) {
// 枚举左边界,左边界的上限设置可以宽松一些
for (int i = 0; i < n; i++) {
// 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
int j = L + i - 1;
// 如果右边界越界,就可以退出当前循环
if (j >= n) {
break;
}
if (s[i] != s[j]) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substr(begin, maxLen);
}
};
1312. 让字符串成为回文串的最少插入次数
数位DP
模版
typedef long long ll;
int a[20];
ll dp[20][state];//dp[pos][pre]不同题目状态不同,比如当前枚举到pos位,前面一位枚举的是pre的个数
ll dfs(int pos,/*state变量*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零
{
//递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了
if(pos==-1) return 1;
//第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];
int up=limit?a[pos]:9;//根据limit判断枚举的上界up;这个的例子前面用213讲过了
ll ans=0;
//开始计数
for(int i=0;i<=up;i++)//枚举
{
if() ...
else if()...
ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos]) //保证合法
/*比如题目要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类,
前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/
}
//计算完,记录状态
if(!limit && !lead) dp[pos][state]=ans;
return ans;
}
ll solve(ll x)
{
int pos=0;
while(x)//把数位都分解出来
{
a[pos++]=x%10;//个人老是喜欢编号为[0,pos)
x/=10;
}
return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);
}
int main()
{
ll le,ri;
while(~scanf("%lld%lld",&le,&ri))
{
memset(dp,-1,sizeof dp);
printf("%lld\n",solve(ri)-solve(le-1)); //分别求区间[0,ri]和[0,le-1]就是[ri,le-1]
}
}
233. 数字 1 的个数
给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数。
class Solution {
public:
int digit[11]; //2^31 是10位 倒序存储数字 个十百012
int dp[11][11];
int countDigitOne(int n) {
//dp[pos][state]:当前位是pos位,已经出现了count个1
if(n<=0) return 0;
memset(dp,-1,sizeof(dp));
int pos = 0;
while(n){
digit[pos++] = n%10;
n /= 10;
}
return dfs(pos-1, 0, true);
}
int dfs(int pos, int count, bool limit){
//边界
if(pos==-1) return count;
//记忆化 !limit使dp状态唯一
if(!limit && dp[pos][count]!= -1) return dp[pos][count]; //没有达到limit
//计数
int res=0;
int max = (limit?digit[pos]:9);
for(int i=0; i<=max; ++i){
res += dfs(pos-1, count+(i==1), limit&&(i==max));
}
//记录状态
if(!limit) dp[pos][count] = res;
return res;
}
};
题目一:数位上不能有4也不能有连续的62
typedef long long ll;
int a[20];
int dp[20][2]; //dp[pos][sta]表示当前第pos位,前一位是否是6的状态
int dfs(int pos,int pre,int sta,bool limit)
{
if(pos==-1) return 1;
if(!limit && dp[pos][sta]!=-1) return dp[pos][sta];
int up=limit ? a[pos] : 9;
int tmp=0;
for(int i=0;i<=up;i++)
{
if(pre==6 && i==2)continue;
if(i==4) continue;//都是保证枚举合法性
tmp+=dfs(pos-1,i,i==6,limit && i==a[pos]);
}
if(!limit) dp[pos][sta]=tmp;
return tmp;
}
int solve(int x)
{
int pos=0;
while(x)
{
a[pos++]=x%10;
x/=10;
}
return dfs(pos-1,-1,0,true);
}
int main()
{
int le,ri;
//memset(dp,-1,sizeof dp);可优化
while(~scanf("%d%d",&le,&ri) && le+ri)
{
memset(dp,-1,sizeof dp);
printf("%d\n",solve(ri)-solve(le-1));
}
return 0;
}
题目二:求F(x)
F(x) = An * 2n-1 + An-1 * 2n-2 + … + A2 * 2 + A1 * 1,Ai是十进制数位,然后给出a,b求区间[0,b]内满足f(i)<=f(a)的i的个数。减法
const int N=1e4+5;
int dp[12][N];
int f(int x)
{
if(x==0) return 0;
int ans=f(x/10);
return ans*2+(x%10);
}
int all;
int a[12];
int dfs(int pos,int sum,bool limit)
{
if(pos==-1) {return sum<=all;}
if(sum>all) return 0;
if(!limit && dp[pos][all-sum]!=-1) return dp[pos][all-sum];
int up=limit ? a[pos] : 9;
int ans=0;
for(int i=0;i<=up;i++)
{
ans+=dfs(pos-1,sum+i*(1<<pos),limit && i==a[pos]);
}
if(!limit) dp[pos][all-sum]=ans;
return ans;
}
int solve(int x)
{
int pos=0;
while(x)
{
a[pos++]=x%10;
x/=10;
}
return dfs(pos-1,0,true);
}
int main()
{
int a,ri;
int T_T;
int kase=1;
scanf("%d",&T_T);
memset(dp,-1,sizeof dp);
while(T_T--)
{
scanf("%d%d",&a,&ri);
all=f(a);
printf("Case #%d: %d\n",kase++,solve(ri));
}
return 0;
}