贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
如何验证可不可以用贪心算法呢?
贪心没有套路,说白了就是常识性推导加上举反例。
手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。
这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end()); // 胃口排序
sort(s.begin(), s.end()); // 饼干尺寸排序
int ans = 0;
int indexS = s.size()-1; // 从后往前遍历
for(int i = g.size()-1; i >= 0; --i){ // for 遍历胃口
if(indexS < 0) break;
if(s[indexS] >= g[i]){ // 贪心,大饼干喂饱大胃口
++ans;
--indexS; // 该饼干满足该胃口,饼干、胃口往左移,否则跳过该胃口,饼干不移动
}
}
return ans;
}
};
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end()); // 胃口排序
sort(s.begin(), s.end()); // 饼干尺寸排序
int ans = 0;
int indexG = 0; // 从前往后遍历
for(int i = 0; i < s.size(); ++i){ // for 遍历饼干
if(indexG == g.size()) break;
if(s[i] >= g[indexG]){ // 贪心,小饼干喂饱小胃口
++ans;
++indexG; // 该饼干满足该胃口,饼干、胃口往右移,否则跳过该饼干,胃口不移动
}
}
return ans;
}
};
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
`
子序列 可以通过从原始序列中删除(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
``
给你一个整数数组 nums ,返回 nums 中 最长摆动子序列的长度 。
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8)。
局部最优:删除单调坡度中间段的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了。
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int size = nums.size();
if(size <= 1) return size;
int ans = 1;
int pre_diff = nums[0] - nums[1]; // 前两个数的差值的相反数,0还是0
for(int i = 1; i < size; ++i){ // 从下标1开始遍历
int curr_diff = nums[i] - nums[i-1]; // 当前差值
// 判断摆动序列,pre = 0只能在开始时才可能出现
if(pre_diff * curr_diff < 0 || (pre_diff == 0 && curr_diff != 0)){
++ans;
pre_diff = curr_diff;
} // 非摆动序列,则跳过不统计,相当于删除操作
}
return ans;
}
};
上面写法将第一个差值取相反数进行判断,
下面写法则设置一个虚拟初始差值为0进行判断,思路更清晰。
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if (nums.size() <= 1) return nums.size();
int curDiff = 0; // 当前一对差值
int preDiff = 0; // 前一对差值
int result = 1; // 记录峰值个数,序列默认序列最右边有一个峰值
for (int i = 0; i < nums.size() - 1; i++) {
curDiff = nums[i + 1] - nums[i];
// 出现峰值
if ((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) {
result++;
preDiff = curDiff;
}
}
return result;
}
};
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
第一层 for 就是设置起始位置,第二层 fo r循环遍历数组寻找最大值,超出时间限制。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if(nums.size() == 0) return 0;
int ans = INT32_MIN;
int count = 0;
for(int i = 0; i < nums.size(); ++i){ // 第一层遍历,以nums[i]为起点
int count = 0;
for(int j = i; j < nums.size(); ++j){ // 第二层遍历,以nums[i]为起点的连续数组和
count += nums[j];
ans = count > ans ? count : ans;
}
}
return ans;
}
};
局部最优:当前连续和count为负数的时候立刻放弃,从下一个元素开始从0重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
这相当于是暴力解法中的不断调整最大子序和区间的起始位置。
区间的终止位置,其实就是如果count取到最大值了,及时记录下来,变相的算是调整了终止位置。
全局最优:选取最大“连续和”
局部最优的情况下,并记录最大的“连续和”,可以推出全局最优。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if(nums.size() == 0) return 0;
int ans = INT32_MIN; // 取nums[0]亦可
int count = 0;
for(int i = 0; i < nums.size(); ++i){
count += nums[i];
if(count > ans) ans = count; // 取区间累计的最大值(相当于不断确定最大子序终止位置)
if(count <= 0) count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
}
return ans;
}
};
dp[i] = max(nums[i], dp[i-1] + nums[i]);
两种选择:以 nums[i] 结尾 或 重新开始 (dp[i-1] < 0);dp[0] = nums[0];
ans
变量保存最大和。class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, 0); // dp[i]表示以nums[i]结尾的最大连续数组和
dp[0] = nums[0];
int ans = nums[0];
for(int i = 1; i < n; ++i){ // 从前往后遍历
dp[i] = max(nums[i], dp[i-1] + nums[i]);
ans = max(dp[i], ans); // 保存最大值
}
return ans;
}
};
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
int pre = nums[0];
int ans = nums[0];
for(int i = 1; i < n; ++i){ // 从前往后遍历
pre = max(nums[i], pre + nums[i]);
ans = max(pre, ans); // 保存最大值
}
return ans;
}
};
给定一个数组 prices ,其中 prices[i] 表示股票第 i 天的价格。
在每一天,你可能会决定购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以购买它,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
输入: prices = [7,1,5,3,6,4]
输出: 7;1->5 + 3->6 = 4 + 3 = 7.
class Solution {
public:
int maxProfit(vector<int>& prices) {
int ans = 0;
int bought = 0;
int cost = 0;
for(int i = 0; i < prices.size()-1; ++i){
if(prices[i+1] >= prices[i] && bought == 0){ // 正收益且未维持时,在上一次购入股票
cost = prices[i];
bought = 1;
}else if(prices[i+1] < prices[i] && bought == 1){ // 负收益且已持有,在上一次卖掉
ans += prices[i] - cost;
bought = 0;
}
}
if(bought == 1) ans += prices.back() - cost; // 到最后一天依然持有,则要算上该次利润
return ans;
}
};
class Solution {
public:
int maxProfit(vector<int>& prices) {
int ans = 0;
for(int i = 0; i < prices.size()-1; ++i){
ans += max(prices[i+1]-prices[i], 0);
}
return ans;
}
};
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<int> dp(n ,0);
for(int i = 1; i < n; ++i){
dp[i] = max(dp[i-1], dp[i-1] + prices[i] - prices[i-1]);
}
return dp[n-1];
}
};
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
在每一个位置跳几步不重要,重要的是最远能到达的位置,这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!
局部最优解:每次取最大跳跃步数(取最大覆盖范围),
整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
class Solution {
public:
bool canJump(vector<int>& nums) {
if(nums.size() == 1) return true;
int nextMax = 0; // 代表当前最大能覆盖的下标值
for(int i = 0; i <= nextMax; ++i){ // i 遍历限制在nextMax之内
nextMax = max(i + nums[i], nextMax); // 不断更新可以到达的最大下标nextMax
if(nextMax >= nums.size()-1) return true; // 判断是否能够覆盖到最后的值
}
return false;
}
};
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
以最小的步数增加最大的覆盖范围,即不断更新下一步能到的最远距离,直到覆盖范围覆盖了终点。
移动下标达到了当前覆盖的最远距离下标且不能到达终点时,步数就要加一,来增加覆盖距离。最后的步数就是最少步数。
除了用currMax
表示当前步数下能到达的最远距离,还要用nextMax
来不断更新下一步能到的最远距离。
class Solution {
public:
int jump(vector<int>& nums) {
int nextMax = 0;
int ans = 0;
int currMax = 0;
for(int i = 0; i < nums.size(); ++i){
nextMax = max(nextMax, i + nums[i]);// 下一步能到的最远距离
if(i == currMax){
if(currMax < nums.size()-1){ // 已经走到该步的最远距离且还没到终点,需要走下一步
++ans; // 走下一步
currMax = nextMax; // 更新能到达的最远距离
if(nextMax >= nums.size()-1) break; // 最远距离覆盖终点,返回
}else break; // 已经走到该步的最远距离并到终点,不需要走下一步
}
}
return ans;
}
};
移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不考虑是不是终点的情况。
想要达到这样的效果,只要让移动下标,最大只能移动到nums.size - 2
的地方就可以了。
因为当移动下标指向nums.size - 2时,有两种情况:
![]() |
![]() |
class Solution {
public:
int jump(vector<int>& nums) {
int nextMax = 0;
int ans = 0;
int currMax = 0;
for(int i = 0; i < nums.size()-1; ++i){ // 逐个遍历到【终点前一个位置】
nextMax = max(nextMax, i + nums[i]);// 下一步能到的最远距离
if(i == currMax){ // 到达该步的最远距离
++ans; // 即走下一步
currMax = nextMax; // 更新能到达的最远距离
} // 如果currMax大于nums.size()-2,即该步覆盖了终点,则不会执行if内语句,不走下一步,直接跳出for循环
}
return ans;
}
};
给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:
选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。
重复这个过程恰好 k 次。可以多次选择同一个下标 i 。
以这种方式修改数组后,返回数组 可能的最大和 。
局部最优:每次反转最小数;
整体最优:整个数组和达到最大。
class Solution {
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
sort(nums.begin(), nums.end()); // 排序
int ans = 0;
for(int i = 0; i < nums.size(); ++i){
if(nums[i] < 0 && k > 0){
nums[i] *= -1; // 在k次范围内把负数先反转掉
--k;
}
ans += nums[i];
}
// k < 0 时,反转完成,不处理
// 【优化:用一个变量记录一下取反之后最小的正整数 可以省去第二次排序】
if(k > 0){ // 全部变为正数,且还剩余反转次数
sort(nums.begin(), nums.end());
if(k % 2 != 0){ // k为偶数时,两次反转相互抵消;
ans -= 2*nums[0]; // k为奇数时,反转最小值,计算总和。
}
}
return ans;
}
};
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
vector<int> rest(gas.size(), 0);
for(int i = 0; i < gas.size(); ++i){
rest[i] = gas[i] - cost[i]; // 记录所有油站的剩余油量
}
for(int i = 0; i < gas.size(); ++i){
if(gas[i] > 0 && rest[i] >= 0){ // 油站油量>0 且 剩余>=0 则可以是起点
int currRest = 0;
for(int j = i; j < gas.size() + i; ++j){
currRest += rest[j % gas.size()];
if(currRest < 0) break; // 剩余量<0 退出
}
if(currRest >= 0) return i; // 剩余量>=0 返回
}
}
return -1;
}
};
直接从全局进行贪心选择,首先从起点0开始遍历,分三种情况如下:
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int min = INT_MAX; // 剩余总油量最低值
int currSum = 0; // 一个循环的油量剩余值
for (int i = 0; i < gas.size(); i++) {
int rest = gas[i] - cost[i]; // 该站剩余油量
currSum += rest;
if(currSum < min) min = currSum;
}
// 1、循环油量剩余值小于零,无法走完
if(currSum < 0) return -1;
// 2、总油量够,且最低剩余油量够,则返回起点0
if(min >= 0) return 0;
// 3、0为起点时不满足,从后往前遍历,当最低剩余油量能够满足时则返回起点i
for(int i = gas.size()-1; i > 0; --i){
int rest = gas[i] - cost[i]; // 该站剩余油量
min += rest;
if(min >= 0) return i;
}
return -1;
}
};
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int sum = 0; // 全程剩余油量
int minSum = INT_MAX; // 最小前缀和
int minIndex = 0; // 最小前缀和节点,终点
for(int i = 0; i < gas.size(); ++i){
int rest = gas[i] - cost[i];
sum += rest;
if(sum < minSum){
minSum = sum;
minIndex = i;
}
}
if(sum < 0) return -1; // 总油量不满足条件
return (minIndex + 1) % gas.size(); // 起点为0时,取模
}
};
i从0开始累加rest[i],和记为currSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,起始位置从i+1算起,再从0计算currSum。
那么为什么一旦[i,j] 区间和为负数,起始位置就可以是j+1呢,j+1后面就不会出现更大的负数?
如果出现更大的负数,就是更新j,那么起始位置又变成新的j+1了。
而且 j 之前出现了多少负数,j 后面就会出现多少正数,因为耗油总和是大于零的(前提我们已经确定了一定可以跑完全程totalSum >= 0
)。
局部最优:当前累加rest[i]
的和currSum
一旦小于0,则更新起始位置为下一个站i+1
开始。
全局最优:找到可以跑一圈的起始位置。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int currSum = 0;
int totalSum = 0;
int start = 0;
for (int i = 0; i < gas.size(); i++) {
int rest = gas[i] - cost[i]; // 该站剩余油量
currSum += rest; // 刷新起点后的总剩余油量
totalSum += rest; // 全周期的总剩余油量
if(currSum < 0){ // 总剩余油量小于零,刷新下一个起点
currSum = 0;
start = i + 1;
}
}
if(totalSum < 0) return -1; // 全周期的总剩余油量不满足,返回-1
return start;
}
};
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
输入:ratings = [1, 2, 2]
输出:4(nums = [1, 2, 1])
一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
一次是从右到左遍历,只比较左边孩子评分比右边大的情况。
这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> nums(ratings.size(), 1); // 对应糖果数组
// 从左往右遍历,比较与左边孩子的评分
for(int i = 1; i < ratings.size(); ++i){
if(ratings[i] > ratings[i-1]){
nums[i] = nums[i-1] + 1;
}
}
// 从右往左遍历,比较与右边孩子的评分
for(int i = ratings.size() - 2; i >= 0; --i){
// 注意当同时满足nums[i] <= nums[i+1]才更新该孩子的糖果数
if(ratings[i] > ratings[i+1] && nums[i] <= nums[i+1]){
nums[i] = nums[i+1] + 1;
}
}
int sum = 0;
for(int i : nums) sum += i;
return sum;
}
};
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int fiveCount = 0; // 剩余5元数量
int tenCount = 0; // 剩余10元数量
for(int num : bills){
if(num == 5){ // 5元直接收账
++fiveCount;
}else if(num == 10){ // 10元找零5元
++tenCount;
--fiveCount;
}else{
if(tenCount){ // 20元优先找零10元+5元
--tenCount;
}else{
fiveCount -= 2;
}
--fiveCount;
}
if(fiveCount < 0) return false; // 无法找零
}
return true;
}
};
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。
每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
本题有两个维度 h 和 k,看到这种题目一定要想如何确定一个维度,然后在按照另一个维度重新排列。
如果按照 k 来从小到大排序,排完之后,会发现 k 的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。
那么按照身高h来排序呢,身高从大到小排,让高个子在前面。(身高相同的话则 k 小的站前面)
此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!
然后按照顺序,以 k 为下标重新插入队列,后序插入节点也不会影响前面已经插入的节点,因为后插入的身高小于已插入的身高,最终按照 k 的规则完成了队列。
局部最优:优先按身高高的people的 k 来插入。插入操作过后的people满足队列属性
全局最优:最后都做完插入操作,整个队列满足题目队列属性
排序完的people: [[7,0], [7,1], [6,1], [5,0], [5,2],[4,4]],按 k 插入的过程为:
class Solution {
public:
static bool comp(const vector<int>& a, const vector<int>& b){ // 自定义二级排序规则
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(), comp); // 排序,身高由大到小,前面人数由小到大
vector<vector<int>> ans;
for(auto person : people){ // 按顺序,在相应位置插入
ans.insert(ans.begin() + person[1], person);
}
return ans;
}
};
遍历过程中,vector 的插入操作消耗较大,可利用链表 list 进行优化。
class Solution {
public:
static bool comp(const vector<int>& a, const vector<int>& b){ // 自定义二级排序规则
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(), comp); // 排序,身高由大到小,前面人数由小到大
list<vector<int>> ans; // list底层是链表实现,插入效率比vector高的多
for(auto person : people){ // 按顺序,在相应位置插入
int pos = person[1];
std::list<vector<int>>::iterator iter = ans.begin(); // 迭代器
while(pos--){
++iter; // 寻找插入位置
}
ans.insert(iter, person);
}
return vector<vector<int>>(ans.begin(), ans.end());
}
};
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。
局部最优:当气球出现重叠,一起射,所用弓箭最少。
全局最优:把所有气球射爆所用弓箭最少。
将气球按照左边界从小到大进行排序,遍历寻找重叠的气球,并更新重叠气球最小右边界。
class Solution {
public:
static bool cmp(vector<int> a, vector<int> b){
return a[0] < b[0]; // 按照xstart从小到大排序
}
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(), points.end(), cmp);
int ans = 1;
int arrow = points[0][1]; // 第一支箭
cout << arrow << endl;
for(int i = 1; i < points.size(); ++i){
if(points[i][0] > arrow){ // 当前新气球的左边界不在重叠气球范围内
++ans; // 则增加一支箭
arrow = points[i][1];
cout << arrow << endl;
}else{ // 当前新气球的左边界在重叠气球的最小右边界范围内
arrow = min(arrow, points[i][1]); // 更新该组重叠气球的最小右边界
}
}
return ans;
}
};
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。 返回 需要移除区间的最小数量,使剩余区间互不重叠 。
局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉。
全局最优:选取最多的非交叉区间。
class Solution {
public:
// 参数要加 & 引用调用,否则超出时间限制
static bool cmp(const vector<int>& a, const vector<int>& b){
return a[1] < b[1];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if (intervals.size() == 0) return 0;
sort(intervals.begin(), intervals.end(), cmp);
int ans = 0;
int n = intervals.size();
int Right = intervals[0][1];
for(int i = 1; i < n; i++){
if(intervals[i][0] < Right){ // 重叠
ans++; // 移除一个右边界更大的,因此右边界不变
}else{
Right = intervals[i][1];// 不重叠,更新右边界
}
}
return ans;
}
};
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
输入:S = “abaedfe”
输出:[3, 4]
class Solution {
public:
vector<int> partitionLabels(string s) {
vector<int> pos(26, -1); // 字母s[i]在第几个区间,下标从0开始
vector<int> ans;
for(int i = 0; i < s.length(); ++i){
if(pos[s[i]-'a'] == -1){ // 新的字母出现
ans.emplace_back(1); // 区间数+1
pos[s[i]-'a'] = ans.size() - 1; // 记录该字母所在区间位置
}else if(pos[s[i]-'a'] == ans.size() - 1){
++ans[pos[s[i]-'a']]; // 该字母在最后一个区间
}else{ // 该字母在前面区间,需合并处理
for(int j = pos[s[i]-'a'] + 1; j < ans.size(); ++j){
ans[pos[s[i]-'a']] += ans[j]; // 将后面区间的字母数累加
}
ans.resize(pos[s[i]-'a'] + 1); // 区间数重置大小
++ans[pos[s[i]-'a']];
for(int j = 0; j < 26; ++j){ // 更新所有字母的所在区间位置
if(pos[j] >= pos[s[i]-'a']){ // 若字母的区间位置>当前字母所在的区间位置
pos[j] = pos[s[i]-'a']; // 则会被合并
}
}
}
}
return ans;
}
};
class Solution {
public:
vector<int> partitionLabels(string s) {
vector<int> pos(26, -1); // 字母s[i]在第几个区间,下标从0开始
vector<int> ans;
for(int i = 0; i < s.length(); ++i){
pos[s[i]-'a'] = i; // 记录所有字母的最远位置
}
int left = 0; // 左边界
int right = 0; // 右边界
for(int i = 0; i < s.length(); ++i){
right = max(pos[s[i]-'a'], right); // 更新区间的最远位置(右边界)
if(right == i){ // 当区间右边界为当前下标时,为分割点
ans.emplace_back(right - left + 1);
left = i + 1; // 更新左边界
}
}
return ans;
}
};
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
按照左边界排序,排序后
局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了;
整体最优:合并所有重叠的区间。
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b){
return a[0] < b[0];
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), cmp); // 按左边界正序排序
int left = intervals[0][0];
int right = intervals[0][1];
vector<vector<int>> ans;
ans.emplace_back(vector<int> {left, right}); // 记录第一个数组区间
for(int i = 1; i < intervals.size(); ++i){
if(intervals[i][0] <= right){ // 重叠
if(intervals[i][1] > right){
right = intervals[i][1]; // 更新右边界
ans.back()[1] = right;
}
}else{
left = intervals[i][0]; // 不重叠
right = intervals[i][1]; // 记录新区间
ans.emplace_back(vector<int> {left, right});
}
}
return ans;
}
};
当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。
class Solution {
public:
bool isIncrease(int n){ // 判断是否为递增数
int backNum = 10;
do{
if(n % 10 > backNum){
return false;
}
backNum = n % 10;
n = n / 10;
}while(n / 10 != 0 || n % 10 != 0);
return true;
}
int monotoneIncreasingDigits(int n) {
for(int i = n; i >=0; --i){
if(isIncrease(i)) return i;
}
return 0;
}
};
局部最优:遇到 str[i - 1] > str[i] 的情况,让str[i - 1]–,然后str[i]给为9,可以保证这两位变成最大单调递增整数。
全局最优:得到小于等于N的最大单调递增的整数。
从后往前遍历,当str[i - 1] > str[i]
时,更新需要更改为 9 的起始位置 pos,并将str[i - 1]--
。
class Solution {
public:
int monotoneIncreasingDigits(int n) {
string str = to_string(n);
int pos = str.length();
for(int i = str.length() - 1; i >= 1; --i){
if(str[i-1] > str[i]){
pos = i;
str[i-1] -= 1;
}
}
for(int i = pos; i < str.length(); ++i){
str[i] = '9';
}
return stoi(str);
}
};
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
将手续费放在买入时进行计算costPrice,初始化为 costPrice = prices[0] + fee;
从第二天开始遍历,
核心思想:假买假卖;
在下一次卖出之前,记录的costPrice都不是真正的买入;
而当我们卖出一支股票(结算利润)时,我们就立即获得了以相同价格并且免除手续费买入一支股票的权利,在下一次买入之前,也不是真正的卖出。
在遍历完整个数组 prices 之后,我们就得到了最大的总收益。
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int costPrice = prices[0] + fee;
int ans = 0;
for(int i = 1; i < prices.size(); ++i){ // 对每一天的股票进行监控操作
if(prices[i] + fee < costPrice){
costPrice = prices[i] + fee; // 成本价(包括手续费)更低,此时买入
}
// prices[i] 落在区间 [costPrice − fee, costPrice] 内,不操作
if(prices[i] <= costPrice && prices[i] + fee >= costPrice) continue;
// 获得利润,计算卖出ans,同时costPrice更新,若有更低价,则卖出成立,否则为继续持有
if(prices[i] > costPrice){
ans += prices[i] - costPrice; // 结算当天利润
costPrice = prices[i]; // 提供反悔操作,即根据之后的价格可选择继续持有而不是真正卖出
}
}
return ans;
}
};
将手续费在股票卖出时进行计算,
pre[0] = max(pre[0], pre[1] + prices[i] - fee);
pre[1] = max(pre[1], pre0 - prices[i]);
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<int> pre(2, 0);
pre[1] = -prices[0];
for(int i = 1; i < prices.size(); ++i){
int pre0 = pre[0]; // 暂存pre[0]
// 未持有 i: i-1 未持有,i 不动; i-1 持有,i 卖出(包含手续费)
pre[0] = max(pre[0], pre[1] + prices[i] - fee);
// 持有 i: i-1 持有,i 不动; i-1 未持有,i 买入
pre[1] = max(pre[1], pre0 - prices[i]); // 可多次买卖
}
return pre[0]; // max(pre[0], pre[1]);
}
};
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例中的摄像头都没有放在叶子节点上,
摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。
所以把摄像头放在叶子节点的父节点位置(中层),才能充分利用摄像头的覆盖面积。
此外我们应从下往上遍历,因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。
局部最优:让叶子节点的父节点安摄像头,所用摄像头最少;
整体最优:全部摄像头数量所用最少!
此时,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后从左、右子树的状态,推导出父节点的状态,隔两个节点放一个摄像头,直至到二叉树头结点。
如何判断某个节点是否需要摄像头?
此时需要进行状态转移(相比动态规划的状态转移无择优过程),节点有三种状态,用数字表示为:
0
:无覆盖,
1
:有摄像头,
2
:无摄像头但覆盖。
在遍历节点时主要有四种情况,
2
,那么此时该节点应返回无覆盖状态 0
,等待上一个节点的覆盖;0
,那么此时该节点应该放置摄像头,返回 1
;1
,那么此时该节点应返回覆盖状态 2;0
,则放置摄像头,ans++。根据以上规则,在遍历到空节点时,应该将空节点的状态返回为 覆盖状态 2
,否则叶子节点将被放置摄像头(空节点为0),或叶子节点的父节点将不放置摄像头(空节点为1)。
class Solution {
public:
int traversal(TreeNode* root, int& ans) { // 状态返回值:0表示无覆盖,1表示有摄像头,2表示无摄像头但覆盖
if(root == nullptr) return 2; // 空节点需要被覆盖
int left = traversal(root->left, ans); // 左节点状态
int right = traversal(root->right, ans);// 右节点状态
if(left == 0 || right == 0){
++ans;
return 1; // 该节点放置摄像头,覆盖子节点
}
if(left == 2 && right == 2){ // 包含叶子节点的情况
return 0; // 左右子节点被覆盖,置0,等待父节点的覆盖
}
if(left == 1 || right == 1){
return 2; // 被子节点覆盖
}
return -1; // 以上判断未使用else,但已包含所有情况,不会执行到该行
}
int minCameraCover(TreeNode* root) {
int ans = 0;
if(traversal(root, ans) == 0){
++ans; // 根结点未被覆盖
}
return ans;
}
};