假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i
,都有一个胃口值 g[i]
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j
,都有一个尺寸 s[j]
。如果 s[j] >= g[i]
,我们可以将这个饼干 j
分配给孩子 i
,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
int g_p=g.size()-1,s_p=s.size()-1;
sort(g.begin(),g.end());
sort(s.begin(),s.end());
int count = 0;
while(s_p>-1 && g_p>-1){
if(g[g_p]<=s[s_p]){
g_p--;
s_p--;
count++;
}else{
g_p--;
}
}
return count;
}
};
为了满足更多的小孩,就不要造成饼干尺寸的浪费。大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。可以尝试使用贪心策略,先将饼干数组和小孩数组排序。然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(),g.end());
sort(s.begin(),s.end());
int index=s.size()-1,res=0;
for(int i=g.size()-1;i>=0;i--){
if(index<0){
break;
}
if(g[i]<=s[index]){
index--;
res++;
}
}
return res;
}
};
时间复杂度:O(nlogn);空间复杂度:O(1)。
从代码中可以看出我用了一个 index 来控制饼干数组的遍历,遍历饼干并没有再起一个 for 循环,而是采用自减的方式,这也是常用的技巧。
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 **摆动序列 。**第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。给你一个整数数组 nums
,返回 nums
中作为 摆动序列 的 最长子序列的长度 。
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if(nums.size()<2){
return nums.size();
}
int curdiff=0;
int prediff=0;
int count=1;
for(int i=0;i<nums.size()-1;i++){
curdiff=nums[i+1]-nums[i];
if((prediff<=0&&curdiff>0)||(prediff>=0&&curdiff<0)){
count++;
prediff=curdiff;//注意这里,只在摆动变化的时候更新prediff
}
}
return count;
}
};
本题异常情况的本质,就是要考虑平坡, 平坡分两种,一个是 上下中间有平坡,一个是单调有平坡,如图:
时间复杂度:O(n);空间复杂度:O(1)
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
暴力解法的思路,第一层 for 就是设置起始位置,第二层 for 循环遍历数组寻找最大值
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int res = INT32_MIN;
int count;
for(int i=0;i<nums.size();i++){
count=0;
for(int j=i;j<nums.size();j++){
count += nums[j];
res = count>res?count:res;
}
}
return res;
}
};//超时
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int res = INT32_MIN;
int count;
for(int i=0;i<nums.size();i++){
if(nums[i]<=0){
continue;
}
count=0;
for(int j=i;j<nums.size();j++){
count += nums[j];
res = count>res?count:res;
}
}
if(res==INT32_MIN){
res=nums[0];
for(int i=1;i<nums.size();i++){
res= res>nums[i]?res:nums[i];
}
}
return res;
}
};
局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。全局最优:选取最大“连续和”。从代码角度上来讲:遍历 nums,从头开始用 count 累积,如果 count 一旦加上 nums[i]变为负数,那么就应该从 nums[i+1]开始从 0 累积 count 了,因为已经变为负数的 count,只会拖累总和。这相当于是暴力解法中的不断调整最大子序和区间的起始位置。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int res=INT32_MIN;
int count=0;
for(int i=0;i<nums.size();i++){
count+=nums[i];
if(count>res){
res=count;
}
if(count<=0){
count=0;
}
}
return res;
}
};
时间复杂度:O(n);空间复杂度:O(1)
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。返回 你能获得的 最大 利润 。
选一个低的买入,再选个高的卖,再选一个低的买入…循环反复。局部最优:收集每天的正利润,全局最优:求得最大利润。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res=0;
for(int i=0;i<prices.size()-1;i++){
res += (prices[i+1]-prices[i]) > 0?(prices[i+1]-prices[i]):0;
}
return res;
}
};
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。
不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。这个范围内,别管是怎么跳的,反正一定可以跳过来。**那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!**每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。
贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
class Solution {
public:
bool canJump(vector<int>& nums) {
int cover=0;
if(nums.size()<=1)
return true;
for(int i=0;i<=cover;i++){
cover=max(i+nums[i],cover);
if(cover>=nums.size()-1){
return true;
}
}
return false;
}
};
不用拘泥于每次究竟跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。
给定一个长度为 n
的 0 索引整数数组 nums
。初始位置为 nums[0]
。每个元素 nums[i]
表示从索引 i
向前跳转的最大长度。换句话说,如果你在 nums[i]
处,你可以跳转到任意 nums[i + j]
处:0 <= j <= nums[i] ;i + j < n。返回到达 nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]
。
贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最少步数。**真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!**如果移动下标达到了当前这一步的最大覆盖最远距离了,还没有到终点的话,那么就必须再走一步来增加覆盖范围,直到覆盖范围覆盖了终点。
移动下标达到了当前覆盖的最远距离下标时,步数就要加一,来增加覆盖距离。最后的步数就是最少步数。
如果当前覆盖最远距离下标不是集合终点,步数就加一,还需要继续走。如果当前覆盖最远距离下标就是集合终点,步数不用加一,因为不能再往后走了。
class Solution {
public:
int jump(vector<int>& nums) {
if(nums.size()<=1)
return 0;
int max_len=0;// 当前覆盖的最大距离
int count = 0;
int next_len=0;// 下一步覆盖最远距离下标
for(int i=0;i<nums.size();i++){
next_len=max(nums[i]+i,next_len);// 更新下一步覆盖最远距离下标
if(i==max_len){ // 遇到当前覆盖最远距离下标
count++;
max_len=next_len;
if(next_len>=nums.size()-1)
break;
}
}
return count;
}
};
时间复杂度: O(n);空间复杂度: O(1)。理解本题的关键在于:以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点,这个范围内最少步数一定可以跳到,不用管具体是怎么跳的,不纠结于一步究竟跳一个单位还是两个单位。
给你一个整数数组 nums
和一个整数 k
,按以下方法修改该数组:选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。重复这个过程恰好 k
次。可以多次选择同一个下标 i
。以这种方式修改数组后,返回数组 可能的最大和 。
贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。
那么本题的解题步骤为:
class Solution {
public:
static bool cmp(int a,int b){
return abs(a)>abs(b);
}
int largestSumAfterKNegations(vector<int>& nums, int k) {
sort(nums.begin(),nums.end(),cmp);
for(int i=0;i<nums.size();i++){
if(k>0 && nums[i]<0){
nums[i] = -nums[i];
k--;
}
}
if(k%2==1) nums[nums.size()-1] = -nums[nums.size()-1];
int res=0;
for(int item:nums){
res+=item;
}
return res;
}
};
在一条环路上有 n
个加油站,其中第 i
个加油站有汽油 gas[i]
升。你有一辆油箱容量无限的的汽车,从第 i
个加油站开往第 i+1
个加油站需要消耗汽油 cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。给定两个整数数组 gas
和 cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1
。如果存在解,则 保证 它是 唯一 的。
暴力的方法很明显就是 O ( n 2 ) O(n^2) O(n2) 的,遍历每一个加油站为起点的情况,模拟一圈。如果跑了一圈,中途没有断油,而且最后油量大于等于0,说明这个起点是ok的。for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
for(int i=0;i<cost.size();i++){
int rest = gas[i]-cost[i];
int index=(i+1)%cost.size();
while(rest>0&&index!=i){// 模拟以i为起点行驶一圈(如果有rest==0,那么答案就不唯一了)
rest+= gas[index]-cost[index];
index=(index+1)%cost.size();
}
// 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置
if(rest>=0 && index==i)
return i;
}
return -1;
}
};//超时
直接从全局进行贪心选择,情况如下:
class Solution {
public:
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;
if(min>=0) return 0;
for(int i=gas.size()-1;i>=0;i--){
int rest=gas[i]-cost[i];
min += rest;
if(min>=0){
return i;
}
}
return -1;
}
};
首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。
那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int cursum=0;
int tosum =0;
int start=0;
for(int i=0;i<gas.size();i++){
cursum+=gas[i]-cost[i];
tosum+=gas[i]-cost[i];
if(cursum<0){ // 当前累加rest[i]和 curSum一旦小于0
start=i+1;// 起始位置更新为i+1
cursum=0;// curSum从0开始
}
}
if(tosum<0) return -1;
return start;
}
};
时间复杂度:O(n);空间复杂度:O(1)
n
个孩子站成一排。给你一个整数数组 ratings
表示每个孩子的评分。你需要按照以下要求,给这些孩子分发糖果:每个孩子至少分配到 1 个糖果。相邻两个孩子评分更高的孩子会获得更多的糖果。请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼。此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> res_vec(ratings.size(),1);
for(int i=1;i<ratings.size();i++){
if(ratings[i]>ratings[i-1]){
res_vec[i] += res_vec[i-1];
}
}
for(int i=ratings.size()-2;i>=0;i--){
if(ratings[i]>ratings[i+1]){
res_vec[i] = max(res_vec[i+1]+1,res_vec[i]);
}
}
int res=0;
for(int res_one:res_vec){
res+=res_one;
}
return res;
}
};
时间复杂度: O(n);空间复杂度: O(n)
那么本题我采用了两次贪心的策略:一次是从左到右遍历,只比较右边孩子评分比左边大的情况。一次是从右到左遍历,只比较左边孩子评分比右边大的情况。这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。
在柠檬水摊上,每一杯柠檬水的售价为 5
美元。顾客排队购买你的产品,(按账单 bills
支付的顺序)一次购买一杯。每位顾客只买一杯柠檬水,然后向你付 5
美元、10
美元或 20
美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5
美元。注意,一开始你手头没有任何零钱。给你一个整数数组 bills
,其中 bills[i]
是第 i
位顾客付的账。如果你能给每位顾客正确找零,返回 true
,否则返回 false
。
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int count5=0,count10=0;
for(int i=0;i<bills.size();i++){
if(bills[i]==5) count5++;
if(bills[i]==10){
if(count5<=0) return false;
count10++;
count5--;
}
if(bills[i]==20){
if(count10>0&&count5>0){
count10--;
count5--;
}else if(count5>=3){
count5-=3;
}else return false;
}
}
return true;
}
};
只需要维护三种金额的数量,5,10和20。有如下三种情况:
所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。
假设有打乱顺序的一群人站成一个队列,数组 people
表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki]
表示第 i
个人的身高为 hi
,前面 正好 有 ki
个身高大于或等于 hi
的人。
请你重新构造并返回输入数组 people
所表示的队列。返回的队列应该格式化为数组 queue
,其中 queue[j] = [hj, kj]
是队列中第 j
个人的属性(queue[0]
是排在队列前面的人)。
如果两个维度一起考虑一定会顾此失彼。如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。那么按照身高h来排序呢,身高一定是从大到小排(身高相同的话则k小的站前面),让高个子在前面。此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!
按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。
所以在按照身高从大到小排序后:局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性;全局最优:最后都做完插入操作,整个队列满足题目队列属性
class Solution {
public:
static bool cmp(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(),cmp);
vector<vector<int>> res;
for(int i=0;i<people.size();i++){
int position = people[i][1];
res.insert(res.begin()+position,people[i]);
}
return res;
}
};
vector& b){
if(a[0]==b[0]) return a[1] return a[0]>b[0];
}
vector
sort(people.begin(),people.end(),cmp);
vector
for(int i=0;i
res.insert(res.begin()+position,people[i]);
}
return res;
}
};