(一)分发饼干
这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
可以尝试使用贪心策略,先将饼干数组和小孩数组排序。
然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。
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;
int result = 0;
for(int i = g.size() - 1; i >= 0; i --){
if(index >=0 && s[index] >= g[i]){
index --;
result ++;
}
}
return result;
}
};
(二)摆动序列
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int result = 1;
int flag = 0; //记录摆动情况,0是第一个,1代表前一个升序,2代表前一个降序
int qian = nums[0];
if(nums.size() == 1){
return result;
}
for(int i = 1; i < nums.size(); i ++){
if((flag == 0 || flag == 2) && nums[i] > qian){
flag = 1;
qian = nums[i];
result ++;
}else if((flag == 0 || flag == 1) && nums[i] < qian){
flag = 2;
qian = nums[i];
result ++;
}else{
// 注意这里还是要更新qian,因为如果一直上升,或一直下降,要取峰顶和谷底,这样可以保留最多的元素
qian = nums[i];
}
}
return result;
}
};
(三)最大子数组和
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = nums[0];
int count = 0;
for(int i = 0; i < nums.size(); i++){
count += nums[i];
if(count > result){
// 注意这里要先给result赋值,针对数组大小为1,且其元素小于0的情况
result = count; //记录当前最好结果
}
if(count <= 0){
count = 0; //当count小于0,对后面为负影响
}
}
return result;
}
};
(四)买卖股票的最佳时机
class Solution {
public:
int maxProfit(vector<int>& prices) {
int result = 0;
for(int i = 1; i < prices.size(); i ++){
result += max(prices[i] - prices[i - 1], 0);
}
return result;
}
};
(五)跳跃游戏
转换为跳跃的覆盖范围是否可以覆盖到终点。
class Solution {
public:
bool canJump(vector<int>& nums) {
int cover = 0;
//注意这里可以遍历到的元素为当前最远可以覆盖到的元素
for(int i = 0; i <= cover; i++){
//更新cover值为当前可以覆盖到的最大元素
cover = max(cover, i + nums[i]);
if(cover >= (nums.size() - 1)){
return true;
}
}
return false;
}
};
(六)跳跃游戏II
用最少的步数,增加覆盖范围,直到覆盖到终点为止,将步数输出。
if(num.size == 1) return 0;
int cur = 0; // 当前覆盖范围
int next = 0; // 下一步覆盖范围
int result = 0;
for(i = 0; i < nums.size(); i ++){
//记录在当前覆盖范围内下一步能跳的最远的距离
next = max(next, i + nums[i]);
if(i == cur){//走到了当前覆盖范围的末位
if(cur != nums.size() - 1){ //不是数组的终点
result ++;
cur = next;
if(cur >= nums.size() - 1) break; //更新后当前覆盖范围已经覆盖全部范围
}else break; //当前覆盖范围已经到终点了
}
}
return result;
class Solution {
public:
int jump(vector<int>& nums) {
if(nums.size() == 1) return 0;
int cur = 0;//当前最大cover
int next = 0; //下一个最大cover
int result = 0;
for(int i = 0; i <= nums.size(); i++){
next = max(next, i + nums[i]);
if(i == cur){
if(cur >= nums.size() - 1){
break;
}else{
result ++;
cur = next;
if(cur >= nums.size() - 1) break;
}
}
}
return result;
}
};
(七)K次取反后最大化的数组和
-
第一步:将数组按照绝对值大小从大到小排序
-
第二步:从前向后遍历,遇到负数将其变为正数,k–
-
第三步:如果K还大于0,且K为奇数(为偶数同一个数翻两次值不变),则反复反转数值最小的元素,将K用完。
-
第四步:求和
class Solution {
static bool cmp(int a, int b){
return abs(a) > abs(b);
}
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
sort(nums.begin(), nums.end(), cmp);
int result = 0;
for(int i = 0; i < nums.size(); i++){
if(nums[i] < 0 && k > 0){
nums[i] = -nums[i];
k --;
}
}
if(k % 2 == 1) nums[nums.size() - 1] *= -1;
for(int i = 0; i < nums.size(); i++){
result += nums[i];
}
return result;
}
};
(八)加油站
- 暴力法:用for循环模拟假设从第i个加油站出发,用while循环模拟环形遍历过程
(超时)
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
for(int i = 0; i < cost.size(); i ++){ //从第i个节点开始走
int rest = gas[i] - cost[i];
int index = (i + 1)%(cost.size());
while(index != i && rest > 0){
rest += gas[index] - cost[index];
index = (index + 1)%cost.size();
}
if(rest >= 0 && index == i){
return i;
}
}
return -1;
}
};
- 贪心
计算补充 → 消耗, 对邮箱的净增
用currentSum累加每个站点剩余的油,如果净增累加(currentSum)起来小于0,就从当前站点的下一个站点开始继续向后遍历,则可能跑完全程(缩小范围,因为从该点前面开始肯定不能跑完全程了)
curSum = 0; //当前消耗,如果当前消耗为负,则从下一个开始(i+1)
totalSum = 0; //整体消耗,如果整体消耗为负数,说明从任何一个位置开始都不可能跑完全程
start = 0; //记录从哪个开始遍历(i+1),可以走完全程
for(int i = 0; i < gas.size(); i ++){
curSum += (gas[i] - cost[i]); //当前剩余总和
totalSum += (gas[i] - cost[i]); //统计总共的剩余
if(curSum < 0 ) { // 说明i前的位置都不适合作为起始位置
start = i + 1;
curSum = 0; // 重新开始这个要归零
}
if(totalSum < 0) return -1;
return start; //如果totalSum >= 0,那么肯定是有解的
}
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int curSum = 0;
int totalSum = 0;
int startIndex = 0;
for(int i = 0; i < gas.size(); i ++){
curSum += (gas[i] - cost[i]);
totalSum += (gas[i] - cost[i]);
if(curSum < 0){
startIndex = i + 1;
curSum = 0;
}
}
if(totalSum < 0) return -1;
return startIndex;
};
};
(九)分发糖果
注意:得分相同,如果没有得分比方便的小孩得分高,则得到的糖果不能比他高
两边一起考虑:顾此失彼
先确定一边,再确定另一边
-
一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
-
一次是从右到左遍历,只比较左边孩子评分比右边大的情况。
默认所有小孩初始都为1
情况1:右半的小孩比左边的小孩高
if(rating[i] > rating[i - 1])
candy[i] = candy[i - 1] + 1;
情况2:左边孩子比右边小孩高
注意:这里必须要从右到左遍历,这样才能利用前一步的结果
因为 rating[5]与rating[4]的比较 要利用上 rating[5]与rating[6]的比较结果,所以 要从后向前遍历。
for(int i = ratings.size() - 2; i >= 0; i --){
if(ratings[i] > ratings[i + 1]){
candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1);
}
}
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> result(ratings.size(), 1);
for(int i = 1; i < ratings.size(); i ++){
if(ratings[i] > ratings[i - 1]){
result[i] = result[i - 1] + 1;
}
}
for(int i = ratings.size() - 2; i >= 0; i --){
if(ratings[i] > ratings[i + 1]){
result[i] = max(result[i], result[i + 1] + 1);
}
}
int sum = 0;
for(int i = 0; i < result.size(); i ++){
sum += result[i];
}
return sum;
}
};
(十)柠檬水找零
只需要维护三种金额的数量,5,10和20。
有如下三种情况:
-
情况一:账单是5,直接收下。
-
情况二:账单是10,消耗一个5,增加一个10
-
情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!
所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
// 分别记录5元、10元、20元
int five = 0, ten = 0, twenty = 0;
for(int i = 0; i < bills.size(); i ++){
if(bills[i] == 5){
five ++;
}
if(bills[i] == 10){
if(five > 0){
five --;
ten ++;
}else{
return false;
}
}
//这里使用了贪心,先用10元的找零,5元用处比较大,能省则省
if(bills[i] == 20){
if(ten > 0 && five > 0){
ten --;
five --;
twenty ++;
}else if(five > 2){
five -= 3;
twenty ++;
}else{
return false;
}
}
}
return true;
}
};
(十一)根据身高重建队列
涉及两个维度,不要同时考虑,确定一个维度后,再确定另一个维度
尝试一: 按照k从小到大排序,对于k相同的,将h较小的排在前面(因为k的含义是,前面有k个比h大的,所以h较大的排在后面,对k没有影响)。→ 两个维度都没有定下来
尝试二: 按照h从大到小排序,对于h相同,k从小到大排序
这样身高已经确定了,根据k调整插入顺序即可(变成插入排序了),而后面的个子矮的人插入到前面不会对前面高的人的k有影响(妙啊)
// 自定义的排序算法(h从大到小排序,对于h相同,k从小到大排序)
sort(people.begin(), people.end(),cmp);
vector<vector<int>> queue;
// 这里必须新开一个vector,因为如果在原来的vector上面插入,则for循环可能重复访问同一个元素(元素位置在插入时改变了)
for(int i = 0; i < people.size(); i ++){
int position = people[i][1]; // 获取k, 即插入位置
queue.insert(queue.begin() + position, people[i]);// 根据k重新插入新队列queue中
}
return queue;
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b){
if(a[0] == b[0]) return b[1] > a[1];
return a[0] > b[0];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(), people.end(), cmp);
vector<vector<int>> queue;
for(int i = 0; i < people.size(); i ++){
int position = people[i][1];
queue.insert(queue.begin() + position, people[i]);
}
return queue;
}
};
(十二)用最少数量的箭引爆气球
从是否需要添加弓箭的角度看这道题目
先按照左边界排序,找到气球重叠
情况一:当两个气球不重叠时,一定需要多添加一个弓箭
情况二:如果两个气球重叠,不需要添加弓箭,只要更新当前气球的右边界为这两个气球的最小右边界。如果下一个气球还小于这个更新的边界,说明这三个气球重叠着,可以一起射穿,不需要添加弓箭
情况三:否则还需要添加弓箭(类似情况一)
if(points.size() == 0) return 0;
sort(points.begin(), points.end(), cmp);
int result = 1; // 气球数目>0,弓箭数至少为1
for(int i = 1; i < points.size(); i ++){// 这里需要i和i - 1比较,所以从1开始
if(points[i][0] > points[i - 1][1]){//两个气球不重叠
result ++;
}else{// 两个气球重叠,更新当前气球的右边界为两者的最小右边界,方便继续判断和下一个气球(第三个)是否重叠
points[i][1] = min(points[i - 1][1], points[i][1]);
}
}
return result;
class Solution {
public:
static bool cmp(const vector<int>&a, const vector<int>&b){
return a[0] < b[0];
}
int findMinArrowShots(vector<vector<int>>& points) {
if(points.size() == 0) return 0;
sort(points.begin(), points.end(), cmp);
int result = 1;
for(int i = 1; i < points.size(); i ++){
if(points[i][0] > points[i - 1][1]){
result ++;
}else{
points[i][1] = min(points[i - 1][1], points[i][1]);
}
}
return result;
}
};
(十三)无重叠区间
和上一题一样的,本质都是找重叠区域,只是上一题是重叠区域则result不用加1,这题是有重叠的就result加1。
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];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if(intervals.size() == 1) return 0;
sort(intervals.begin(), intervals.end(), cmp);
int result = 0;
for(int i = 1; i < intervals.size(); i ++){
if(intervals[i][0] >= intervals[i - 1][1]){
continue;
}else{
result ++;
//删掉那个右边界比较大的区间,这样对后面的影响小
intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]);
}
}
return result;
}
};
(十四)划分字母区间
在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。(这个区间必须包含之前遍历过的字母的最远边界,因为同一个字母只能出现在一个区间内,而要这个区间划分数最多,所以就去这个最远边界,这里要注意最远边界是随着字母的遍历动态变化的)。
class Solution {
public:
vector<int> partitionLabels(string s) {
int hash[26] = {0};
vector<int> result;
for(int i = 0; i < s.size(); i ++){
hash[s[i] - 'a'] = i;
}
int left = 0;
int right = 0;
for(int i = 0; i < s.size(); i ++){
right = max(right, hash[s[i] - 'a']);
if(i == right){
int temp = right - left + 1;
result.push_back(temp);
left = i + 1;
}
}
return result;
}
};
(十五)合并区间
还是同类题目,变化是这里要先push一个进入result中,然后决定是更新result中最后一个vector的结束区间,还是新Push进去一个区间
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) {
if(intervals.size() == 1) return intervals;
sort(intervals.begin(), intervals.end(), cmp);
vector<vector<int>> result;
result.push_back(intervals[0]);
for(int i = 1; i < intervals.size(); i ++){
if(intervals[i][0] > result[result.size() - 1][1]){
result.push_back(intervals[i]);
}else{
// 这里需要取重叠区间最后结束的(并集)
result[result.size() - 1][1] = max(result[result.size() - 1][1], intervals[i][1]);
}
}
return result;
}
};
(十六)单调递增的数字
那么从后向前遍历,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,就可以重复利用上次比较得出的结果。从后向前遍历332的数值变化为:332 -> 329 -> 299
class Solution {
public:
int monotoneIncreasingDigits(int n) {
// 将数字转为字符串,方便逐位处理
string strNum = to_string(n);
// 细节,即使中间的某一位小于后一位,只要前面有一个变成了9,后面的都必须变成9
// eg: 328 -> 298 -> 299
int flag = strNum.size();
for(int i = strNum.size() - 2; i >=0; i --){
if(strNum[i] > strNum[i + 1]){
flag = i + 1;
strNum[i] = strNum[i] - 1;
}
}
for(int i = flag; i < strNum.size(); i ++){
strNum[i] = '9';
}
// stoi函数可以自动去除前导0
return stoi(strNum);
}
};
(十七)监控二叉树
优先在叶子节点上的父节点上放置摄像头,因为叶子节点比较多,这样可以节省更多的摄像头。之后每隔两个空节点放一个摄像头
需要从下往上遍历二叉树(后序:左-右-中)
根据左右节点的状态确定父节点的状态,模拟每隔两个节点放摄像头的过程
0:无覆盖
1:有摄像头
2:有覆盖
注:空节点的状态:有覆盖,确保叶子节点的父亲节点时有覆盖状态
状态转移,同时统计摄像头数量
1.左右孩子都有覆盖2
其父节点为无覆盖0(等着这个父节点的父节点装摄像头,给他变成覆盖)
2.左右孩子至少有一个为无覆盖0
其父节点一定装摄像头1,覆盖其孩子
3.左右孩子至少有一个有摄像头1
父节点一定是有覆盖状态2
(注意2、3顺序不能换,因为只要左右孩子中有一个无覆盖,即使其中一个孩子装着摄像头,还是要给其父节点添加摄像头)
4.根节点的特殊处理,因为根节点上面没有父节点了,他的无覆盖状态无法改变
如果最后根节点还是无覆盖状态,因此要另外给他加个摄像头
int result;
// 因为这里一个节点的状态是由他的孩子节点的状态决定的,所以递归需要返回值
int traversal(TreeNode* cur){
// 终止条件,如果遇到空节点,就向上返回
if(cur == NULL) return 2; // 空节点为有覆盖状态
int left = traversal(cur -> left); // 左孩子状态
int right = traversal(cur -> right); // 右孩子状态
if(left == 2 && right == 2) return 0; // 情况1
if(left == 0 || right == 0){ // 情况2
result ++; // 该节点装一个摄像头
return 1;
}
if(left == 1 || right == 1) return 2; //情况3
return -1; // 这里不会走到
}
// 主函数中处理情况4
int camera(root){
result = 0;
if(traversal(root) == 0){ // 最终root的状态返回为无覆盖
result ++;
}
return result;
}
/**
* 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 {
private:
int result;
int traversal(TreeNode* cur){
if(cur == NULL) return 2;
int left = traversal(cur -> left);
int right = traversal(cur -> right);
if(left == 2 && right == 2) return 0;
if(left == 0 || right == 0){
result ++;
return 1;
}
if(left == 1 || right == 1) return 2;
return -1;
}
public:
int minCameraCover(TreeNode* root) {
result = 0;
if(traversal(root) == 0) result ++;
return result;
}
};