目录
1--贪心算法
主要思路:
通过局部最优推导全局最优;
2--分发饼干
主要思路:
基于贪心算法,每次都尽可能用大的饼干去喂胃口大的孩子,贪心地节省饼干;
#include <iostream>
#include <vector>
#include <algorithm>
class Solution {
public:
int findContentChildren(std::vector<int>& g, std::vector<int>& s) {
if(s.size() == 0) return 0;
// 从小到大排序饼干
std::sort(g.begin(), g.end());
std::sort(s.begin(), s.end());
int res = 0; // 结果
int cookie_idx = s.size() - 1; // 从最后一个饼干开始分发
// 贪心地每次用大饼干去喂胃口大的孩子
for(int child_idx = g.size() - 1; child_idx >= 0; child_idx--){
if(cookie_idx >= 0 && s[cookie_idx] >= g[child_idx]){ // 饼干能满足孩子的胃口
res++;
cookie_idx--;
}
}
return res;
}
};
int main(int argc, char* argv[]){
// g = [1,2,3], s = [1,1]
std::vector<int> child = {1, 2, 3};
std::vector<int> cookie = {1, 1};
Solution S1;
int res = S1.findContentChildren(child, cookie);
std::cout << res << std::endl;
return 0;
}
3--摆动序列
主要思路:
基于贪心算法,贪心地计算山谷和山峰的个数;
#include <iostream>
#include <vector>
class Solution {
public:
int wiggleMaxLength(std::vector<int>& nums) {
int pre_diff = 0;
int cur_diff = 0;
int result = 1; // 默认最右边元素是一个摆动
for(int i = 0; i < nums.size() - 1; i++){
cur_diff = nums[i+1] - nums[i];
if(pre_diff <= 0 && cur_diff > 0 || pre_diff >= 0 && cur_diff < 0){
result ++;
pre_diff = cur_diff; // 产生摆动才更新 pre_diff 避免平波递增的情况
}
}
return result;
}
};
int main(int argc, char* argv[]){
// nums = [1,7,4,9,2,5]
std::vector<int> test = {1, 7, 4, 9, 2, 5};
Solution S1;
int res = S1.wiggleMaxLength(test);
std::cout << res << std::endl;
return 0;
}
4--最大子序和
主要思路:
贪心地纳入当前数,如果和为负值,就将和置为 0,否则保留;
#include <iostream>
#include <vector>
#include <limits.h>
class Solution {
public:
int maxSubArray(std::vector<int>& nums) {
int sum = 0;
int max_sum = INT_MIN;
for(int i = 0; i < nums.size(); i++){
sum = sum + nums[i];
if(sum > max_sum) max_sum = sum;
if(sum < 0) sum = 0;
}
return max_sum;
}
};
int main(int argc, char* argv[]){
// nums = [-2,1,-3,4,-1,2,1,-5,4]
std::vector<int> nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
Solution S1;
int res = S1.maxSubArray(nums);
std::cout << res << std::endl;
return 0;
}
5--买卖股票最佳时机II
#include <iostream>
#include <vector>
class Solution {
public:
int maxProfit(std::vector<int>& prices) {
int maxProfit = 0;
for(int i = 1; i < prices.size(); i++){
int profit = prices[i] - prices[i-1];
if(profit > 0) maxProfit += profit;
}
return maxProfit;
}
};
int main(int argc, char* argv[]){
// prices = [7,1,5,3,6,4]
std::vector<int> prices = {7, 1, 5, 3, 6, 4};
Solution S1;
int res = S1.maxProfit(prices);
std::cout << res;
return 0;
}
主要思路:
基于贪心算法,贪心地计算当天利润(当天价格减去前一天价格)(总感觉怪怪的,但又举不出反例),只贪心地考虑正利润;
6--跳跃游戏
主要思路:
贪心地计算当前的最大覆盖范围,当跳跃覆盖范围包含最后一个下标时,返回True;
#include <iostream>
#include <vector>
class Solution {
public:
bool canJump(std::vector<int>& nums) {
int cover = 0;
for(int cur = 0; cur <= cover; cur++){
cover = std::max(cover, cur + nums[cur]);
if(cover >= nums.size() - 1) return true;
}
return false;
}
};
int main(int argc, char* argv[]){
// nums = [2,3,1,1,4]
std::vector<int> nums = {2, 3, 1, 1, 4};
Solution S1;
int res = S1.canJump(nums);
if(res) std::cout << "true" << std::endl;
else std::cout << "false" << std::endl;
return 0;
}
7--跳跃游戏II
主要思路:
再不需要额外走多一步的情况下,贪心地计算能够到达最远的地方(在当前覆盖范围里);
假如目前已经走到上一步能够到达最远的地方了,只能在上一次的覆盖范围内再额外走多一步,这时覆盖范围会更新;判断当前覆盖范围是否包含终点;
#include <iostream>
#include <vector>
class Solution {
public:
int jump(std::vector<int>& nums) {
if(nums.size() == 1) return 0;
int cover_dis = 0;
int pre_dis = 0;
int res = 0;
for(int i = 0; i < nums.size(); i++){
cover_dis = std::max(cover_dis, i + nums[i]); // 最远可以到达的地方
if(i == pre_dis){
res++; // 已经走到上一次记录时,能够走到最远的地方,只能额外向前走多一步了
pre_dis = cover_dis; // 额外走多一步,可以走到最远的地方
if(pre_dis >= nums.size() - 1) break; // 额外走多一步,已经可以覆盖终点了,直接返回
}
}
return res;
}
};
int main(int argc, char* argv[]){
// nums = [2, 3, 1, 1, 4]
std::vector<int> nums = {2, 3, 1, 1, 4};
Solution S1;
int res = S1.jump(nums);
std::cout << res << std::endl;
return 0;
}
8--K次取反后最大化的数组和
主要思路:
对数组按绝对值大小进行排序,贪心地先对绝对值最大的负数进行取反;对所有负数取反后,假如还剩下取反次数,则对绝对值最小的数进行取反;
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
class Solution {
public:
int largestSumAfterKNegations(std::vector<int>& nums, int k) {
// 按绝对值从大到小排序
std::sort(nums.begin(), nums.end(), [](int a, int b)->bool{
return std::abs(a) > std::abs(b);
});
for(int i = 0; i < nums.size(); i++){
if(nums[i] < 0 && k > 0){
nums[i] = nums[i]*(-1);
k--;
}
}
if(k % 2 == 1){ // k 为奇数,对绝对值最小的进行取反
nums[nums.size() - 1] *= -1;
}
return std::accumulate(nums.begin(), nums.end(), 0);
}
};
int main(int argc, char* argv[]){
// nums = [4,2,3], k = 1
std::vector<int> test = {4, 2, 3};
int k = 1;
Solution S1;
int res = S1.largestSumAfterKNegations(test, k);
std::cout << res << std::endl;
return 0;
}
9--加油站
主要思路:
基于贪心算法,视频讲解:加油站
#include <iostream>
#include <vector>
class Solution {
public:
int canCompleteCircuit(std::vector<int>& gas, std::vector<int>& cost) {
int start = 0, cur_sum = 0, tol_sum = 0;
for(int i = 0; i < gas.size(); i++){
cur_sum += gas[i] - cost[i]; // 经过当前加油站后剩下的油(假设能跑一圈)
tol_sum += gas[i] - cost[i]; // 累积剩下的油
if(cur_sum < 0){ // 正常从加油站0出发,到达不了该加油站
start = i + 1; // 尝试从第一个不能到达的加油站的下一个加油站出发
cur_sum = 0;
}
}
if(tol_sum < 0) return -1; // 跑完全程的累积剩油量为负数,返回-1
return start; // 剩油量为正数,表明在第一个不能到达的加油站之后,经过剩下的加油站,剩油量为正数
}
};
int main(int argc, char* argv[]){
// gas = [1,2,3,4,5], cost = [3,4,5,1,2]
std::vector<int> gas = {1, 2, 3, 4, 5};
std::vector<int> cost = {3, 4, 5, 1, 2};
Solution S1;
int res = S1.canCompleteCircuit(gas, cost);
std::cout << res << std::endl;
return 0;
}
10--分发糖果
主要思路:
基于贪心算法,从左到右遍历,当一个孩子比左边的孩子高时,贪心地只多分发一个糖果(相对于左边的孩子);
再从右到左遍历,当一个孩子比右边的孩子高时,贪心地只多分发一个糖果(相对于右边的孩子),一个孩子分发的糖果取决于上述两个遍历过程的最大值;
#include <iostream>
#include <vector>
#include <numeric>
class Solution {
public:
int candy(std::vector<int>& ratings) {
// 从左到右遍历
std::vector<int> candy(ratings.size(), 1); // 初始化全部最小分发一个糖果
for(int i = 0; i < ratings.size(); i++){
if(i > 0 && ratings[i] > ratings[i-1]){ // 比左边的孩子高,贪心地只多分一个
candy[i] = candy[i-1] + 1;
}
}
// 从右到左遍历
for(int i = ratings.size() - 1; i >= 0; i--){
if(i < ratings.size() - 1 && ratings[i] > ratings[i+1]){ // 比右边的孩子高,贪心地只多分一个
candy[i] = std::max(candy[i], candy[i+1] + 1);
}
}
return std::accumulate(candy.begin(), candy.end(), 0);
}
};
int main(int argc, char* argv[]){
// ratings = [1,0,2]
std::vector<int> test = {1, 0, 2};
Solution S1;
int res = S1.candy(test);
std::cout << res << std::endl;
return 0;
}
11--柠檬水找零
主要思路:
基于贪心算法,当遇到 20 美元时,贪心地尽可能保留 5 美元,使用一张 10 美元和一张 5 美元来找零;
#include <iostream>
#include <vector>
class Solution {
public:
bool lemonadeChange(std::vector<int>& bills) {
int five = 0, ten = 0, twenty = 0;
for(int i = 0; i < bills.size(); i++){
if(bills[i] == 5){
five++;
}
else if(bills[i] == 10 && five > 0){
ten++;
five--;
}
else if(bills[i] == 20 && ten > 0 && five > 0){ // 优先使用 1 张 10 美元和 1 张 5 美元找零
twenty++;
ten--;
five--;
}
else if(bills[i] == 20 && five >= 3){
twenty++;
five -= 3;
}
else{
return false;
}
}
return true;
}
};
int main(int argc, char* argv[]){
// bills = [5,5,5,10,20]
std::vector<int> nums = {5, 5, 5, 10, 20};
Solution S1;
bool res = S1.lemonadeChange(nums);
if(res) std::cout << "true" << std::endl;
else std::cout << "false" << std::endl;
return 0;
}
12--根据身高重建队列
主要思路:
题目的意思是对于重构后的队列,每个人前面只有 k_i 个人比其高;
可以先对数组队列按照身高进行从大到小排序(身高相同,k小的排在前面),然后遍历排序后的数组队列,按照 k_i 插入到对应的位置;
因为已经按身高进行排序了,因此可以确保遍历到的元素,其前面的元素肯定都是更高的,因此插入到前面指定位置 position,也能确保前面有 position 个人相对更高;
#include <iostream>
#include <vector>
#include <algorithm>
class Solution {
public:
std::vector<std::vector<int>> reconstructQueue(std::vector<std::vector<int>>& people) {
std::sort(people.begin(), people.end(), [](std::vector<int> a, std::vector<int> b){
if (a[0] == b[0]) return a[1] < b[1]; return a[0] > b[0];});
std::vector<std::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;
}
};
int main(int argc, char* argv[]){
// people = [[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]
std::vector<std::vector<int>> test = {{7, 0}, {4, 4}, {7, 1}, {5, 0}, {6, 1}, {5, 2}};
Solution S1;
std::vector<std::vector<int>> res = S1.reconstructQueue(test);
for(auto vec : res){
for(int val : vec){
std::cout << val << " ";
}
std::cout << std::endl;
}
return 0;
}
13--用最少数量的箭引爆气球
主要思路:
基于贪心算法,贪心地将尽可能多的气球重叠在一起,使用一支箭引爆重叠的气球;
#include <iostream>
#include <vector>
#include <algorithm>
class Solution {
private:
static bool cmp(const std::vector<int>& a, const std::vector<int>& b) {
return a[0] < b[0];
}
public:
int findMinArrowShots(std::vector<std::vector<int>>& points) {
if (points.size() == 0) return 0;
// 按左边界进行排序,使重叠的气球紧邻
std::sort(points.begin(), points.end(), cmp);
int res = 0;
for(int i = 0; i < points.size(); i++){
if(i > 0 && points[i][0] <= points[i-1][1]){ // 左边界小于上一个气球的右边界,表明两者重叠
points[i][1] = std::min(points[i][1], points[i-1][1]); // 更新重叠区域的右边界
}
else{ // 不重叠
res++;
}
}
return res;
}
};
int main(int argc, char* argv[]){
// points = [[10,16],[2,8],[1,6],[7,12]]
std::vector<std::vector<int>> test = {{10, 16}, {2, 8}, {1, 6}, {7, 12}};
Solution S1;
int res = S1.findMinArrowShots(test);
std::cout << res << std::endl;
return 0;
}
14--无重叠区间
主要思路:
对区间进行排序,让尽可能多的区间重叠在一起,判断重叠区间的个数,移除重叠区间,返回结果即可;
#include <iostream>
#include <vector>
#include <algorithm>
class Solution {
public:
int eraseOverlapIntervals(std::vector<std::vector<int>>& intervals) {
std::sort(intervals.begin(), intervals.end(),
[](std::vector<int>& a, std::vector<int>& b){
return a[0] < b[0]; // 按左边界从小到大排序
});
int res = 0;
for(int i = 1; i < intervals.size(); i++){
if(intervals[i][0] < intervals[i-1][1]){ // 重叠
res++;
intervals[i][1] = std::min(intervals[i][1], intervals[i-1][1]); // 移除一个区间后,更新右边界
}
else{ // 不重叠
continue;
}
}
return res;
}
};
int main(int argc, char* argv[]){
// intervals = [[1,2],[2,3],[3,4],[1,3]]
std::vector<std::vector<int>> intervals = {{1, 2}, {2, 3}, {3, 4}, {1, 3}};
Solution S1;
int res = S1.eraseOverlapIntervals(intervals);
std::cout << res << std::endl;
return 0;
}
15--划分字母区间
主要思路:
用哈希表记录每一个字母最远出现的位置,然后遍历字符串,记录遍历区间中字母的最远位置,当遍历位置到达区间字母的最远位置时,表明包含了区间中所有的字母,记录区间的大小;
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <algorithm>
class Solution {
public:
std::vector<int> partitionLabels(std::string s) {
std::unordered_map<char, int> hash_map;
for(int i = 0; i < s.length(); i++){
hash_map[s[i]] = i; // 覆盖更新字母出现过的最远位置
}
int cur_right = 0;
std::vector<int> res;
int start = 0;
for(int i = 0; i < s.length(); i++){
cur_right = std::max(hash_map[s[i]], cur_right); // 当前遍历的字母里的最远位置
if(cur_right == i){ // 当前已经遍历到最远位置了
res.push_back(i - start + 1); // 字母的个数
start = i+1; // 更新左边界
}
}
return res;
}
};
int main(int argc, char* argv[]){
// s = "ababcbacadefegdehijhklij"
std::string test = "ababcbacadefegdehijhklij";
Solution S1;
std::vector<int> res = S1.partitionLabels(test);
for(int val : res) std::cout << val << " ";
std::cout << std::endl;
return 0;
}
16--合并区间
主要思路:
对区间按左边界进行排序,遍历区间,如果当前区间与上一个区间不重叠,则记录上一个区间;如果当前区间与上一个区间重叠,则合并和更新当前区间的范围;
#include <iostream>
#include <vector>
#include <algorithm>
class Solution {
public:
std::vector<std::vector<int>> merge(std::vector<std::vector<int>>& intervals) {
// 对区间按左边界进行排序,让区间尽可能重叠
std::sort(intervals.begin(), intervals.end(),
[](std::vector<int> &a, std::vector<int> &b){
return a[0] < b[0];
});
std::vector<std::vector<int>> res;
int i;
for(i = 1; i < intervals.size(); i++){
if(intervals[i][0] > intervals[i - 1][1]){ // 不重叠
res.push_back(intervals[i - 1]);
}
else{ // 重叠,合并区间
intervals[i][0] = std::min(intervals[i-1][0], intervals[i][0]);
intervals[i][1] = std::max(intervals[i-1][1], intervals[i][1]);
}
}
// 记录最后一个区间
res.push_back(intervals[i-1]);
return res;
}
};
int main(int argc, char* argv[]){
// intervals = [[1,3],[2,6],[8,10],[15,18]]
std::vector<std::vector<int>> test = {{1, 3}, {2, 6}, {8, 10}, {15, 18}};
Solution S1;
std::vector<std::vector<int>> res = S1.merge(test);
for(auto vec : res){
for(int val : vec) std::cout << val << " ";
std::cout << std::endl;
}
return 0;
}
17--单调递增的数字
主要思路:
从后向前遍历数字,记录不是单调递增的位置,将该位置的数值减1;
从记录位置向后遍历,将所有数值贪心地更改为 9;
#include <iostream>
#include <string>
class Solution {
public:
int monotoneIncreasingDigits(int n) {
std::string str = std::to_string(n);
int flag = str.size();
for(int i = str.size() - 2; i >= 0; i--){
if(str[i] > str[i+1]){ // 不符合单调递增
flag = i; // 记录位置
str[flag] = str[flag] - 1;
}
}
for(int i = flag + 1; i < str.size(); i++){
str[i] = '9';
}
return atoi(str.c_str());
}
};
//test
int main(int argc, char *argv[]) {
int n = 332;
Solution S1;
int res = S1.monotoneIncreasingDigits(n);
std::cout << res << std::endl;
return 0;
}
18--监控二叉树
主要思路:
对于每一个结点,有三种状态:无覆盖(2)、有覆盖但没安装摄像头(1)、有覆盖且安装摄像头(0);
为了计算最小摄像头数量,采用后序遍历,从底至上;
对于 NULL 结点(叶子结点的孩子)应置为 1 状态,对于叶子结点应置为 1 状态,对于叶子节点的父亲,应置为 0 状态;
对于每一个结点,当有一个孩子处于 2 状态时,将在这结点安装摄像头,即设置为 0 状态;当两个孩子都处于 1 状态时,将这结点设置为 2 状态;当两个孩子一个处于 1 状态,另一个处于 0 状态,则将结点设置为 1 状态;
#include <iostream>
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:
int minCameraCover(TreeNode* root) {
int flag = dfs(root);
if(flag == 2) num++; // 根节点处于无监控状态
return num;
}
int dfs(TreeNode *root){
if(root == nullptr) return 1; // 有监控状态
int left = dfs(root->left); // 左
int right = dfs(root->right); // 右
// 根
if(left == 2 || right == 2){ // 有一个孩子处于无监控状态,必须安装摄像头
num++;
return 0;
}
if(left == 1 && right == 1){ // 两个孩子都处于有监控状态,但都没安装摄像头
return 2; // 表示此时的节点是处于无监控状态的
}
// 一个孩子装有摄像头,另一个孩子处于监控状态,或两个孩子都装有摄像头
if((left == 1 && right == 0) || (left == 0 && right == 1) || (left == 0 && right == 0)){
return 1; // 表示当前结点是处于有监控状态的
}
return -1; // 出错情况
}
private:
int num = 0;
};
int main(int argc, char *argv[]) {
// [0, 0, null, 0, 0]
TreeNode* Node1 = new TreeNode(0);
TreeNode* Node2 = new TreeNode(0);
TreeNode* Node3 = new TreeNode(0);
TreeNode* Node4 = new TreeNode(0);
Node1->left = Node2;
Node2->left = Node3;
Node2->right = Node4;
Solution S1;
int res = S1.minCameraCover(Node1);
std::cout << res << std::endl;
return 0;
}