代码随想录刷题记录
贪心算法
分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
用尽量小的饼干喂饱孩子。
摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int res = 1;
vector<vector<int>> dp(nums.size(), vector<int>(2, 1));
// dp[i][0] 代表 nums[i]作为谷底的最长摆动序列长度
// dp[i][1] 代表 nums[i]作为谷顶的最长摆动序列长度
for (int i = 0; i < nums.size(); ++i) {
int sheng = 0, jiang = 0;
for (int j = i - 1; j >= 0; --j) {
if (nums[i] > nums[j]) {
sheng = max(sheng, dp[j][0]);
} else if (nums[i] < nums[j]) {
jiang = max(jiang, dp[j][1]);
}
}
dp[i][0] += jiang;
dp[i][1] += sheng;
res = max({res, dp[i][0], dp[i][1]});
}
return res;
}
};
最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
将数组作为dp就好,记录当前元素结尾的最大和连续子数组。
跳跃游戏
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
记录目前能达到的最右边界,如果当前位置加上跳跃长度大于边界,就更新边界。
跳跃游戏 II
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
maxPos记录目前能达到的最右边界,如果当前位置加上跳跃长度大于边界,就更新边界。
end记录当前步数能达到最右边界,当到达end时,步数加一,并且将end更新为maxPos
K次取反后最大化的数组和
给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)
以这种方式修改数组后,返回数组可能的最大和。
先把绝对值大的负数 变为负数,若全部负数都变为正数。剩下的k还为奇数,就选一个绝对值最小的数变为负数。
加油站
在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
class Solution {
public:
/*
直接从全局进行贪心选择,情况如下:
情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的
情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。
*/
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int curSum = 0;
int min = INT_MAX; // 从起点出发,油箱里的油量最小值
for (int i = 0; i < gas.size(); i++) {
int rest = gas[i] - cost[i];
curSum += rest;
if (curSum < min) {
min = curSum;
}
}
if (curSum < 0) return -1; // 情况1
if (min >= 0) return 0; // 情况2
// 情况3
for (int i = gas.size() - 1; i >= 0; i--) {
int rest = gas[i] - cost[i];
min += rest;
if (min >= 0) {
return i;
}
}
return -1;
}
};
分发糖果
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:
- 每个孩子至少分配到 1 个糖果。
- 相邻的孩子中,评分高的孩子必须获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?
class Solution {
public:
// 两次贪心。第一次从左往右。第二次从右往左
int candy(vector<int>& ratings) {
vector<int> candiesVec(ratings.size(), 1);
for (int i = 1; i < ratings.size(); ++i) {
if (ratings[i] > ratings[i - 1]) {
candiesVec[i] = candiesVec[i - 1] + 1;
}
}
for (int i = ratings.size() - 2; i >= 0; --i) {
if (ratings[i] > ratings[i + 1]) {
// 这里一定要加判断,要去最大值
candiesVec[i] = max(candiesVec[i], candiesVec[i + 1] + 1);
}
}
return accumulate(candiesVec.begin(), candiesVec.end(), 0);
}
};
柠檬水找零
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。
顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
优先用10去找零。
根据身高重建队列
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
// 版本一
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b) {
// 将身高高的排前面,身高相同的,[1]小的放前面
if (a[0] == b[0]) return a[1] < b[1];
return a[0] > b[0];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort (people.begin(), people.end(), cmp);
vector<vector<int>> que;
for (int i = 0; i < people.size(); i++) {
int position = people[i][1];
que.insert(que.begin() + position, people[i]);
}
return que;
}
};
用最少数量的箭引爆气球
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(), points.end(), [](const vector<int>& a, const vector<int>& b) {
// 以区间结尾排序
return a[1] < b[1];
});
int shotpos = points[0][1], shotcnt = 1;
for (auto& vec : points) {
if (vec[0] > shotpos) {
++shotcnt;
shotpos = vec[1]; /*以区间结尾为射击位置,尽可能射更多的气球*/
}
}
return shotcnt;
}
};
无重叠区间
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:
- 输入: [ [1,2], [2,3], [3,4], [1,3] ]
- 输出: 1
- 解释: 移除 [1,3] 后,剩下的区间没有重叠。
先让右边界小的排除,这样的区间会影响尽可能少的区间。
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), [](const vector<int>& lhs, const vector<int>& rhs) {
return lhs[1] < rhs[1];
});
int cnt = 0, end = INT_MIN;
for (const auto& interval : intervals) {
if (interval[0] >= end) {
end = interval[1];
} else {
++cnt;
}
}
return cnt;
}
};
划分字母区间
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
示例:
- 输入:S = "ababcbacadefegdehijhklij"
- 输出:[9,7,8] 解释: 划分结果为 "ababcbaca", "defegde", "hijhklij"。 每个字母最多出现在一个片段中。 像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。
记录每个字母最后一次出现的位置,然后双指针start,end。移动i,若map[s[i]]比end大,更新end。当i等于end,表明区间出现,记录长度。
合并区间
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
- 输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
- 输出: [[1,6],[8,10],[15,18]]
- 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
将区间按照左边界从小到大排序。然后遍历,合并
单调递增的数字
给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。
(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)
示例 1:
- 输入: N = 10
- 输出: 9
class Solution {
public:
int monotoneIncreasingDigits(int N) {
string strNum = to_string(N);
// flag用来标记赋值9从哪里开始
// 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行
int flag = strNum.size();
for (int i = strNum.size() - 1; i > 0; i--) {
// 找到第一个大于其后面的数字,将其减一
if (strNum[i - 1] > strNum[i] ) {
flag = i;
strNum[i - 1]--;
}
}
// 将其后面的数字都置为9
for (int i = flag; i < strNum.size(); i++) {
strNum[i] = '9';
}
return stoi(strNum);
}
};
监控二叉树
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
动态规划
爬楼梯
斐波那契数
爬楼梯
这两道都是斐波那契数列
使用最小花费爬楼梯
- 题目:
给你一个整数数组
cost,其中cost[i]是从楼梯第i个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为
0或下标为1的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。
- 定义:dp[i]代表爬到第i个台阶所需最小花费,
- 遍历: dp[i] = min(dp[i - 1] + cost[i - 1] , dp[i - 2] + cost[i-2] ) . return dp[n]
背包类问题
01背包
分割等和子集
- 题目: 转换成01背包,数组的值即是重量,也是价值。背包大小为数组总和的一半,求背包能装的最大价值是否为数组总和一半
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200
示例 1:
- 输入: [1, 5, 11, 5]
- 输出: true
- 解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
- 输入: [1, 2, 3, 5]
- 输出: false
- 解释: 数组不能分割成两个元素和相等的子集.
-
定义:dp[i] [j] 在 (0 ~ i - 1) 件物品, 背包大小为j,能装的最大价值
-
初始化 dp[n + 1] [halfsum + 1] =
-
遍历:注意j的遍历顺序,从后往前,否则前面的物品会被取多次,变成完全背包
for (i = 1; i <= n; ++i) {
for (j = halfsum; j >= 1; ++j) {
if (j >= nums[i - 1]) {
dp[j] = max(dp[j], dp[j - nums[i-1]] + nums[i - 1]);
}
}
}
最后一块石头的重量II
- 题目:这还是01背包,石头价值和重量,将所有石头重量一半作为背包大小,这样尽可能装多的石头,这样最后:abs(allweight - dp[halfweight] - dp[halfweight])
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
示例:
- 输入:[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],这就是最优值。
- 略
目标和
- 题目:注意转变思路:一半加+,一半加-,也就是 left + right = sum; left - right = target; left = (sum + target)/ 2。则还是变为01背包,在数组中取一些数,其总和等于left。特别注意,题目要求的是组合数(这里理解就算值相同,下标不同就是不同的数)。
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例:
- 输入:nums: [1, 1, 1, 1, 1], S: 3
- 输出:5
解释:
- -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
一共有5种方法让最终目标和为3。
提示:
- 数组非空,且长度不会超过 20 。
- 初始的数组的和不会超过 1000 。
- 保证返回的最终结果能被 32 位整数存下。
- 定义:dp[i] [j] 代表 nums[0 ~ i-1]这些数中,组成和为j的组合数一共有dp[i] [j]种方法
- 初始化:dp[i] [0] = 1; dp[0] [j] = 0; dp[0] [0] = 1;
- 遍历:
for (i = 1; i <=n; ++i) {
for (j = left; j >= 1; --j) {
dp[i][j] += dp[i-1][j - nums[i]];
// 一维:dp[j] += dp[j - nums[i]];
}
}
return dp[left];
一和零
- 题目:其实还是01背包,不过这个背包是3维的。第1维度还是物品,2和3维度则是0和1的个数
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
- 输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
- 输出:4
- 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
- 定义:dp[a] [b] [c] 代表 在0 ~ a -1 个物品中,最多b个0,c个1,最大子集数量
- 初始化:dp[k] [m] [n] =
- 遍历:注意遍历顺序,m和n是从大到小
for (a = 1; a < k; ++a) {
for (b = m; b >= 1; ++b) {
for (c = n; c >= 1; ++c) {
if (strs[i-1]的0个数 <= b && strs[i-1]的1个数 <= c)
dp[i][b][c] = max(dp[i][b][c], dp[i-1][b - strs[i-1]的0个数][strs[i-1]的1个数] + 1);
}
}
}
- 优化:其实a的那层完全没有必要,可以将最外层去掉,因为都是算完i-1再算i。(由m和n是从大到小 保证,如果不是,则需要添加多一层循环)并且都是先算完小的dp[m] [n]再算大的,所以不用担心覆盖
完全背包
零钱兑换II
- 题目:"每一种面额的硬币有无限个"就代表完全背包,完全背包与01背包的就是内层遍历背包大小的循环换过来。01:从大到小,完全:从小到大。题目求的是组合数
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 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
注意,你可以假设:
- 0 <= amount (总金额) <= 5000
- 1 <= coin (硬币面额) <= 5000
- 硬币种类不超过 500 种
- 结果符合 32 位符号整数
- 定义:dp[i] [j] 代表用 0~i-1种硬币,凑成面额为j的组合数
- 初始化dp[i] [j] = 0; dp[0] [0] = 1;
- 遍历
for (i = 1; i <= n; ++i) {
for (j = 1; j <= amount; ++j) {
if (coins[i-1] <= j) {
dp[j] += dp[j-coins[i-1]];
}
}
}
组合总和 Ⅳ
- 题目:完全背包,而且求的是排列数。
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
- nums = [1, 2, 3]
- target = 4
所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)
请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。
- 定义:dp[i] [j] 从 0到i-1种硬币,组成面额j的排列数
- 初始化:dp[i] [0] = 1, else = 0
- 遍历
对于完全背包“
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
for (j = 1; j <= target; ++j) {
for (i = 1; i <= n; ++i) {
if (j >= coins[i - 1]) {
dp[i][j] += dp[i-1][j-conis[i-1]];
}
}
}
return dp[n][target]
爬楼梯(进阶版)
- 题目:1-m个台阶代表物品,目前台阶j代表背包大小,”多少种方法“代表有先后顺序,而且是完全背包,则与上面的 组合求和Ⅳ相同。注意先遍历背包,再遍历物品。
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
输入描述:输入共一行,包含两个正整数,分别表示n, m
输出描述:输出一个整数,表示爬到楼顶的方法数。
输入示例:3 2
输出示例:3
提示:
当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。
此时你有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶段
- 1 阶 + 2 阶
- 2 阶 + 1 阶
零钱兑换
- 题目:"每种硬币的数量是无限的"代表完全背包。而且是组合数,从而确定遍历顺序
给定不同面额的硬币 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
- 定义:dp[i] [j] >= 0 代表可以用 0 到 i-1种硬币凑成面额j用的最少硬币数。为INT_MAX则代表不能。
- 初始化:dp[i] [0] 全部为0, 其余都为INT_MAX
- 遍历:求组合,而且完全背包。这道题目要注意:你尽量不用二维矩阵做(下面有做),因为dp[i] [j] 依赖的很复杂他不仅依赖dp[i-1] [j - coin], 而且还依赖最新的 dp[i] [j - coin],所以你每次都需要新复制上一行,那这样不如用一维的,而且注意背包的遍历顺序,从小到大,因为大的依赖小的。建议与 零钱兑换Ⅱ 对比一下。
for (i = 1; i <= n; ++i) {
for (j = amount; j >= 0; --j) {
if (j >= coins[i-1]) {
dp[i][j] = min(dp[i][j], dp[i-1][j-coins[i-1]]+1);
}
}
}
return dp[n][amount] != INT_MAX ? dp[n][amount] : -1;
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
if (amount == 0) return 0;
int n = coins.size();
vector<long long> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i < n; ++i) {
for (int j = coins[i]; j <= amount; ++j) {
dp[j] = min(dp[j], dp[j-coins[i]]+1);
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
// 二维
vector<vector<long long>> dp(n + 1, vector<long long>(amount + 1, INT_MAX));
for (int j = 1; j <= amount; ++j) dp[0][j] = INT_MAX; // 没有硬币
for (int i = 0; i <= n; ++i) dp[i][0] = 0; // 总额为0
for (int i = 1; i <= n; ++i) {
dp[i] = dp[i-1]; // 注意这一行
for (int j = coins[i-1]; j <= amount; ++j) {
dp[i][j] = min({dp[i][j], dp[i][j-coins[i-1]]+1});
}
}
return dp[n][amount] != INT_MAX ? dp[n][amount] : -1;
}
};
完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
平方数作为物品,先遍历物品,再从小到大遍历背包,就是完全背包,因为每个平方数可以使用多次
class Solution {
public:
int numSquares(int n) {
// if (sqrt(n) * sqrt(n) == n) return 1;
// dp[i]代表和为i的完全平方数的最少数量
vector<int> dp(n + 1);
dp[0] = 0;
for (int i = 1; i <= n; ++i) {
dp[i] = i;
}
for (int i = 1, square = i * i; square <= n; ++i, square = i * i) {
for (int j = square; j <= n; ++j) {
if (dp[j - square] + 1 < dp[j]) {
dp[j] = dp[j - square] + 1;
}
}
}
return dp[n];
}
};
打家劫舍
打家劫舍
dp[i] = (dp[i - 2] + nums[i], dp[i - 1])
打家劫舍II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。
去除头和去除尾,用打家劫舍1的方法求两次,求最大值就可以
打家劫舍 III
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
class Solution {
public:
int rob(TreeNode* root) {
if (!root) return 0;
if (!root->left && !root->right) {
return root->val;
}
if (cache[root]) {
return cache[root];
}
int rootval = root->val;
if (root->left) rootval += rob(root->left->left) + rob(root->left->right); // 不偷root->left
if (root->right) rootval += rob(root->right->left) + rob(root->right->right); // 不偷root->right
int nonrootval = rob(root->left) + rob(root->right); // 不偷root
int res = max(rootval, nonrootval);
cache[root] = res; // 记录当前节点为根,能盗取的最大金额
return res;
}
private:
unordered_map<TreeNode*, int> cache;
int res = 0;
};
买股票问题
买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
- 示例 1:
- 输入:[7,1,5,3,6,4]
- 输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
初始时,将第一天作为买入的时候,后面遍历数组,当大于买入价格时,记录差值。当小于买入价格时,更新买入时间。
买卖股票的最佳时机II
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- 示例 1:
- 输入: [7,1,5,3,6,4]
- 输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
class Solution {
public:
int maxProfit(vector<int>& prices) {
// dp[i][0] 表示第i天持有股票所得现金。
// dp[i][1] 表示第i天不持有股票所得最多现金
int len = prices.size();
vector<vector<int>> dp(len, vector<int>(2, 0));
dp[0][0] -= prices[0];
dp[0][1] = 0;
// 持有股票需要和持有股票的状态比较
// 没买股票需要和没买股票的状态比较
for (int i = 1; i < len; i++) {
// dp[i - 1][0]: 继续持有股票,dp[i - 1][1] - prices[i]:第i天买入股票
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
// dp[i - 1][1]:继续不买股票,dp[i - 1][0] + prices[i]:第i天卖出股票
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return dp[len - 1][1];
}
};
买卖股票的最佳时机III
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- 示例 1:
- 输入:prices = [3,3,5,0,0,3,1,4]
- 输出:6 解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3。
class Solution {
public:
int maxProfit(vector<int>& prices) {
// dp[i][0]: 没有任何操作
// dp[i][1]: 第一次持有股票
// dp[i][2]: 第一次不持有股票
// dp[i][3]: 第二次持有股票
// dp[i][4]: 第二次不持有股票
int len = prices.size();
// 第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢?
// 第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后再买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。
// 所以第二次买入操作,初始化为:dp[0][3] = -prices[0];
vector<vector<int>> dp(len, vector<int>(5, 0));
dp[0][1] = dp[0][3] = -prices[0];
// dp[0][0] = dp[0][1] = dp[0][3] = 0;
// 持有股票需要和持有股票的状态比较
// 没买股票需要和没买股票的状态比较
for (int i = 1; i < len; i++) {
dp[i][0] = dp[i - 1][0];
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
return max({dp[len - 1][0], dp[len - 1][2], dp[len - 1][4]});
}
};
买卖股票的最佳时机IV
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- 示例 1:
- 输入:k = 2, prices = [2,4,1]
- 输出:2 解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2。
和Ⅲ一样
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
if (prices.size() == 0) return 0;
// dp[i][0]: 没有任何操作
// dp[i][2*k-1]: 第k次持有股票
// dp[i][2*k]: 第k次不持有股票
vector<vector<int>> dp(prices.size(), vector<int>(2 * k + 1, 0));
for (int i = 1; i < 2 * k + 1; i += 2) dp[0][i] = -prices[0];
for (int i = 1; i < prices.size(); i++) {
for (int j = 1; j < 2 * k + 1; j += 2) {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] + prices[i]);
}
}
return dp[prices.size() - 1][2 * k];
}
};
子序列或连续序列问题
最长递增子序列
- 题目
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
- dp定义:dp[i] 代表 以nums[i]为结尾的最长递增子序列的长度
- 初始化:dp数组都为1
- 遍历:
for (i = 1; i < n; ++i) {
for (j = 0; j < i; ++j) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
res = max(res, dp[i]);
}
}
最长连续递增序列
- 题目:
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
- dp定义:dp[i] 代表 以nums[i]为结尾的最长连续递增序列的长度
- 初始化:dp数组都为1
- 遍历:
for (i = 0; i < n - 1; ++i) {
if (nums[i + 1] > nums[i])
dp[i + 1] = dp[i] + 1;
}
最长重复子数组
- 题目:注意,这里是子数组,不是子序列
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
示例:
输入:
- A: [1,2,3,2,1]
- B: [3,2,1,4,7]
- 输出:3
- 解释:长度最长的公共子数组是 [3, 2, 1] 。
- dp定义:dp[ i ] [ j ] 代表 以 A[i-1]和B[j-1]为结尾的最长的子数组的长度
- 初始化:dp[0] [j] = dp[i] [0] = 0
- 遍历
dp [lena+1][lenb+1];
for (i = 1; i <= lena; ++i) {
for (j = 1; j <= lenb; ++j) {
if (A[i - 1] == B[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
// /*这里是子序列的写法*/dp[i][j] = max(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]);
dp[i][j] = 0;
}
}
}
最长公共子序列
- 题目:
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
- 输入:text1 = "abcde", text2 = "ace"
- 输出:3
- 解释:最长公共子序列是 "ace",它的长度为 3。
- dp定义:dp[i] [j] 代表text1[0 ~ i-1] 和 text2[0 ~ j-1]两个子数组最长公共子序列的长度
- 初始化:dp[0] [j] = dp[i] [0] = 0
- 遍历
dp[n][m]; n:text1.size m:text2.size
for (i = 1; i <= n; ++i) {
for (j = 1; j <= m; ++j) {
if (text1[i-1] == text2[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1; // 二维
// dp[j] = dp[j-1] + 1; // 一维
} else {
dp[i][j] = max({dp[i - 1][j], dp[i][j-1]}); // 二维
// dp[j] = max({dp[j], dp[j-1]}); // 一维
}
}
}
不相交的线
- 题目:这道题主要是理解,找规律,注意不能相交这个点。连线要在两个相同的数字,不能相交:要求按照相对顺序来。那不就是找按相对顺序的相同数字的对数,那不就是求最长子序列。
我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。
现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。
以这种方法绘制线条,并返回我们可以绘制的最大连线数。
- dp定义:dp[i] [j] 为 A[0 ~ i-1] 和 B[0 ~ j-1] 最长相同子序列的长度
- 初始化:dp[0] [j] = dp[i] [0] = 0
- 遍历
for (i = 1; i <= n; ++i) {
for (j = 1; j <= m; ++j) {
if (A[i - 1] == B[i - 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[n][m]
判断子序列
- 题目:其实还是求公共子序列。求s和t的最长子序列,看是不是为s本身
- 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1:
- 输入:s = "abc", t = "ahbgdc"
- 输出:true
示例 2:
- 输入:s = "axc", t = "ahbgdc"
- 输出:false
提示:
- 0 <= s.length <= 100
- 0 <= t.length <= 10^4
两个字符串都只由小写字符组成。
不同的子序列
- 题目:
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)
题目数据保证答案符合 32 位带符号整数范围。
- dp定义:dp[i] [j] 代表 s[0 ~ i-1] 的子序列中出现 t[0 ~ j-1]的个数
- 初始化:dp[0] [j] = dp[i] [0] = 0, dp[0] [0] = 1
- 遍历:
n = s.size();
m = t.size();
for (i = 1; i <= n; ++i) {
for (j = 1; j <= m; ++j) {
if (s[i-1] == t[j-1]) {
dp[i][j] = dp[i-1][j] /*s[i-1]不参与匹配*/ + dp[i-1][j-1]/*s[i-1]参与匹配*/;
} else {
dp[i][j] = dp[i-1][j] /*s[i-1]不参与匹配*/
}
}
}
return dp[n][m]
编辑距离
两个字符串的删除操作
- 题目:
- 给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
示例:
- 输入: "sea", "eat"
- 输出: 2
- 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
- dp定义:dp[i] [j] 代表 word1[0 ~ i-1] 和 word2[0 ~ j-1] 相同需要的最小步数
- 初始化:dp[0] [j] = j ; dp[i] [0] = i ; dp[0] [0] = 0
- 遍历:
for (i = 1; i <= n; ++i) {
for (j = 1; j <= m; ++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]+1, dp[i][j-1]+1, dp[i-1][j-1]+2});
}
}
}
return dp[n][m]
编辑距离
- 题目:
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
- 示例 1:
- 输入:word1 = "horse", word2 = "ros"
- 输出:3
- 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')
- 示例 2:
- 输入:word1 = "intention", word2 = "execution"
- 输出:5
- 解释: intention -> inention (删除 't') inention -> enention (将 'i' 替换为 'e') enention -> exention (将 'n' 替换为 'x') exention -> exection (将 'n' 替换为 'c') exection -> execution (插入 'u')
- dp定义:dp[i] [j] 代表 word1[0 ~ i-1] 和 word2[0 ~ j-1] 相同需要的最小编辑次数步数
- 初始化:dp[0] [j] = j ; dp[i] [0] = i ; dp[0] [0] = 0
- 遍历:
for (i = 1; i <= n; ++i) {
for (j = 1; j <= m; ++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]+1/*删除或修改word1[i-1]*/,
dp[i][j-1]+1/*删除或修改word2[j-1]*/,
/*没必要*/ dp[i-1][j-1]+2/*删除word1[i-1]和word2[i-2]*/,
});
}
}
}
return dp[n][m]
回文
回文子串
- 题目:这道题dp不好作,用双指针遍历更简单
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
- 输入:"abc"
- 输出:3
- 解释:三个回文子串: "a", "b", "c"
class Solution {
public:
int helper(const string& s, int left, int right) {
int ret = 0;
while (left >= 0 && right < s.size()) {
if (s[left] == s[right]) {
--left;
++right;
++ret;
} else {
break;
}
}
return ret;
}
int countSubstrings(string s) {
int ret = 0;
for (int i = 0; i < s.size(); ++i) {
ret += helper(s, i, i + 1);
ret += helper(s, i, i);
}
return ret;
}
};
最长回文子序列
- 题目: 注意是子序列,不是连续的
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1: 输入: "bbbab" 输出: 4 一个可能的最长回文子序列为 "bbbb"。
示例 2: 输入:"cbbd" 输出: 2 一个可能的最长回文子序列为 "bb"。
- dp定义:dp[i] [j] 代表 以s[i ~ j]的最长回文子序列的最长长度
- 初始化:dp[i] [i] = 1; else = 0
- 遍历:这里注意遍历顺序。(因为上一行(i)是依赖下一行(i+1),所以需要先计算下一行)。并且 i > j 时,dp[i] [j] 无意义,所以j是从i+1开始
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i + 1; j < s.size(); 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][s.size() - 1];
}
};

浙公网安备 33010602011771号