贪心算法理论基础
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
这一类的题目也没什么固定套路,理论上的求解过程如下:
贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
做题的时候,一般就是想想看本题的局部最优是怎么求解的,解出来看看是否满足全局最优,举几个例子如果都不是反例,那就可以用贪心算法做出来了。
分发饼干
例题455(简单)分发饼干
注意要点:
- 我的思路是,用最小的饼干满足胃口,所以从最小的饼干开始遍历,直到有一块饼干满足了胃口最小的人,就换下一个人来,饼干则继续遍历;
- 就是说,for来控制饼干的遍历,而满足了能喂饱,人的序号就移动到下一个;
- 为了满足这种算法,两个数组都要先经过排序。
文字的叙述不是很好说,简单而言就是小饼干先去喂饱小胃口!
下面贴出代码:
CPP版本
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int index = 0;
for (int i = 0; i < s.size(); i++)
{
if (index < g.size() && g[index] <= s[i]) {index++;}
}
return index;
}
};
C版本
int cmp(const int* a, const int* b)
{
return *a - *b;
}
int findContentChildren(int* g, int gSize, int* s, int sSize){
//先排序
qsort(g, gSize, sizeof(int), cmp);
qsort(s, sSize, sizeof(int), cmp);
int index = 0;
for (int i = 0; i < sSize; i++)
{
if (index < gSize && s[i] >= g[index]) {index++;}
}
return index;
}
摆动序列
例题376(中等)摆动序列
注意要点:
- 如果序列中是单调的,那么就只有首尾需要记录,中间的都可以删除;
- 可通过cur和pre来记录当前和之前的元素差,只有pre和cur相反才需要记录和更新(pre可以=0)。
纯文字叙述可能不是一下子能模拟出来,我把代码随想录上面这道题的精华图拉到这里,可以看一下,再配合文字和代码进行理解:
下面贴出代码:
CPP版本
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if (nums.size() == 1) {return nums.size();}
int curdiff = 0;
int prediff = 0;
int ans = 1;
for (int i = 1; i < nums.size(); i++)
{
curdiff = nums[i] - nums[i - 1];
if ((prediff <= 0 && curdiff > 0) || (prediff >= 0 && curdiff < 0))
{
ans++;
prediff = curdiff;
}
}
return ans;
}
};
C版本
int wiggleMaxLength(int* nums, int numsSize){
if (numsSize < 2) {return numsSize;}
int prediff = 0, curdiff = 0;
int result = 1;
for (int i = 0; i < numsSize - 1; i++)
{
curdiff = nums[i + 1] - nums[i];
if ((prediff <= 0 && curdiff > 0) || (prediff >= 0 && curdiff < 0))
{
result++;
prediff = curdiff;
}
}
return result;
}
最大子数组和
例题53(中等)最大子数组和
注意要点:
- 通过count来记录当前的连续数组的和,如果比我的结果result大就更新进去;
- count如果<0,那么就可以新选择一个子数组的头,也就是更新count=0。
这道题其实不是很好想,记一下这个代码的逻辑会用就可以了,一下子没想出来很正常的。
下面贴出代码:
CPP版本
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int ans = INT32_MIN;
int count = 0;
for (const int& num : nums)
{
count += num;
if (count > ans) {ans = count;}
if (count < 0) {count = 0;}
}
return ans;
}
};
C版本
int maxSubArray(int* nums, int numsSize){
int count = 0;
int result = INT_MIN;
for (int i = 0; i < numsSize; i++)
{
count += nums[i];
result = count > result ? count : result;
if (count < 0) {count = 0;}
}
return result;
}
买卖股票的最佳时机II
例题122(中等)买卖股票的最佳时机II
注意要点:
- 题目强调了当天可以买进卖出同时进行,暗示可以直接贪心法结题;
- 买卖股票的利润就是i+1与i对应的价格的差,如果i+1的>i的价格,就可以统计,否则就跳过即可。
下面贴出代码:
CPP版本
class Solution {
public:
int maxProfit(vector<int>& prices) {
int ans = 0;
for (int i = 0; i < prices.size() - 1; i++)
{
int now = prices[i + 1] - prices[i];
ans += now > 0 ? now : 0;
}
return ans;
}
};
C版本
int maxProfit(int* prices, int pricesSize){
int result = 0;
for (int i = 1; i < pricesSize; i++)
{
if (prices[i] > prices[i - 1])
{
result += prices[i] - prices[i - 1];
}
}
return result;
}
跳跃游戏
例题55(中等)跳跃游戏
注意要点:
- 本题的重点不在于跳跃方式,而是跳跃的成功与否,所以能否达到才是关键;
- 重点就是当前格子跳跃后能覆盖到的范围,能覆盖到才需要继续遍历,不然就相当于失败了(没有覆盖到最后面,那就是跳不到)。
下面贴出代码:
CPP版本
class Solution {
public:
bool canJump(vector<int>& nums) {
int n = nums.size();
int cover = 0;
if (n == 1) {return 1;}
for (int i = 0; i <= cover; i++)
{
cover = max(nums[i] + i, cover);
if (cover >= n - 1) {return 1;}
}
return 0;
}
};
C版本
bool canJump(int* nums, int numsSize){
int cover = 0;
if (numsSize < 2) {return true;}
for (int i = 0; i <= cover; i++)
{
int now = nums[i] + i;
cover = now > cover ? now : cover;
if (cover >= numsSize - 1) {return true;}
}
return false;
}
例题45(中等)跳跃游戏II
注意要点:
- 同样也是通过覆盖距离来完成代码逻辑;
- 当且仅当当前位置来到了之前覆盖距离的边界,才刷新下一步的最远覆盖距离。
下面贴出代码:
CPP版本
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
if (n == 1) {return 0;}
int ans = 0;
int cur = 0, next = 0;
for (int i = 0; i < n; i++)
{
next = max(nums[i] + i, next);
if (i == cur)
{
ans++;
if (next >= n - 1) {break;}
cur = next;
}
}
return ans;
}
};
C版本
int jump(int* nums, int numsSize){
if (numsSize == 1) {return 0;}
int cur = 0;
int ans = 0;
int next = 0;
for (int i = 0; i < numsSize; i++)
{
next = (nums[i] + i) > next ? (nums[i] + i) : next;
if (i == cur)
{
cur = next;
ans++;
if (next >= numsSize - 1) {break;}
}
}
return ans;
}
k次取反后最大化的数组和
例题1005(简单)k次取反后最大化的数组和
注意要点:
- 这道题应该很好想,就是每次都先翻转绝对值最大的负数;
- 翻转完了还有剩,就重复翻转现在最小的元素直到达到翻转次数;
- 要注意的是,C++里面sort的cmp要加static!
下面贴出代码:
CPP版本
class Solution {
private:
static int cmp(const int a, const int b)
{
return abs(a) > abs(b);
}
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
sort(nums.begin(), nums.end(), cmp);
int n = nums.size();
for (int i = 0; i < n; i++)
{
if (nums[i] < 0 && k)
{
nums[i] = -nums[i];
k--;
}
}
if (k % 2) {nums[n - 1] = -nums[n - 1];}
int ans = 0;
for (const int& num : nums) {ans += num;}
return ans;
}
};
C版本
int cmp(const int* a, const int* b)
{
return abs(*b) - abs(*a);
}
int largestSumAfterKNegations(int* nums, int numsSize, int k){
qsort(nums, numsSize, sizeof(int), cmp);
int ans = 0;
for (int i = 0; i < numsSize; i++)
{
if (nums[i] < 0 && k)
{
nums[i] = -nums[i];
k--;
}
}
if (k % 2) {nums[numsSize - 1] = -nums[numsSize - 1];}
for (int i = 0; i < numsSize; i++) {ans += nums[i];}
return ans;
}
加油站
例题134(中等)加油站
注意要点:
- 先说暴力解法(leetcode超时):通过for循环遍历起始加油站,然后通过index=(i + 1)%size来完成从起点开始的遍历,如果能遍历回到当前i且剩余油量>=0那么现在的i就是所求;
- 贪心法非常巧妙:统计每一个加油站点的剩余油量,如果站点i油量<0,那么只可能从i+1开始作为起始站点;
- 最后的判断,就是总的剩余油量>=0,那么贪心法求得的start就是所求;否则本题就无解返回-1。
纯文字叙述不够直观,我把代码随想录的图片拉下来,可以更清晰的理解这个局部求最优的算法:
下面贴出代码:
CPP版本
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int cur = 0;
int total = 0;
int start = 0;
for (int i = 0; i < gas.size(); i++)
{
cur += gas[i] - cost[i];
total += gas[i] - cost[i];
if (cur < 0)
{
start = i + 1;
cur = 0;
}
}
if (total < 0) {return -1;}
return start;
}
};
C版本
int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize){
int cur = 0, total = 0;
int start = 0;
for (int i = 0; i < gasSize; i++)
{
cur += gas[i] - cost[i];
total += gas[i] - cost[i];
if (cur < 0)
{
start = i + 1;
cur = 0;
}
}
if (total < 0) {return -1;}
return start;
}
分发糖果
例题135(困难)分发糖果
注意要点:
- 这道题难就难在两边的大小都要考虑,想要一次性全部写出逻辑代码非常困难;一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边;
- 遍历,如果更大那就比前一个人的糖果数+1;
- 第二次遍历,就需要比较,如果你的前一个人没你大,那么你的糖果数就应该是前一个人+1和你当前糖果数的较大值。
这道题很难靠直接头脑中的模拟直接做出来,我这儿再次白嫖代码随想录的示意图,可以更清晰直观的看出解题的步骤:
下面贴出代码:
CPP版本
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> candy(ratings.size(), 1);
for (int i = 1; i < candy.size(); i++)
{
if (ratings[i] > ratings[i - 1]) {candy[i] = candy[i - 1] + 1;}
}
for (int i = candy.size() - 2; i >= 0; i--)
{
if (ratings[i] > ratings[i+1]) {candy[i] = max(candy[i+1]+1, candy[i]);}
}
int sum = 0;
for (const int& num : candy) {sum += num;}
return sum;
}
};
C版本
int candy(int* ratings, int ratingsSize){
//贪心两次
int* candy = (int* )malloc(sizeof(int) * ratingsSize);
candy[0] = 1;
for (int i = 1; i < ratingsSize; i++)
{
if (ratings[i] > ratings[i - 1]) {candy[i] = 1 + candy[i - 1];}
else {candy[i] = 1;}
}
int sum = 0;
for (int i = ratingsSize - 1; i > 0; i--)
{
sum += candy[i];
if (ratings[i] < ratings[i - 1])
{
candy[i - 1] = candy[i - 1] > (candy[i] + 1) ? candy[i - 1] : candy[i] + 1;
}
}
sum += candy[0];
return sum;
}
柠檬水找零
例题860(简单)柠檬水找零
注意要点:
- 纸币的面值越小就越有用,所以优先使用10元找零。
下面贴出代码:
CPP版本
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five = 0, ten = 0, twenty = 0;
for (int bill : bills)
{
if (bill == 5) {five++;}
else if (bill == 10)
{
if (five <= 0) {return 0;}
ten++;
five--;
}
else
{
if (five && ten)
{
five--;
ten--;
}
else if (five >= 3) {five -= 3;}
else return 0;
}
}
return 1;
}
};
C版本
bool lemonadeChange(int* bills, int billsSize){
int* types = (int* )malloc(sizeof(int) * 2);
types[0] = types[1] = 0;
for (int i = 0; i < billsSize; i++)
{
if (bills[i] == 5)
{
types[0]++;
}
else if (bills[i] == 10)
{
types[1]++;
if (types[0] <= 0) {return false;}
types[0]--;
}
else
{
if (types[1])
{
types[1]--;
if (types[0] <= 0) {return false;}
types[0]--;
}
else if (types[0] < 3) {return false;}
else types[0] -= 3;
}
}
return true;
}
根据身高重建队列
例题406(中等)根据身高重建队列
注意要点:
- 同样有两个元素要考虑,所以要先考虑一个顺序,再考虑另一个;
- 优先考虑身高,保证身高是从高到低排列的;
- 贪心法精髓的局部最优在这里体现:按照k来决定当前数组插入到答案数组的下标位置,保证局部完成k的要求。
下面贴出代码:
CPP版本
class Solution {
private:
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];
}
public:
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(), people.end(), cmp);
list<vector<int>> que;
for (int i = 0; i < people.size(); i++)
{
int pos = people[i][1];
list<vector<int>>:: iterator it = que.begin();
while (pos--) {it++;}
que.insert(it, people[i]);
}
return vector<vector<int>> (que.begin(), que.end());
}
};
C版本
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
int cmp(const void* a, const void* b)
{
int* tmp1 = *(int** )a;
int* tmp2 = *(int** )b;
//身高从高到低,如果一样,k从小到大
return tmp1[0] == tmp2[0] ? tmp1[1] - tmp2[1] : tmp2[0] - tmp1[0];
}
void move(int** people, int peopleSize, int start, int end)
{
for (int i = end; i > start; i--)
{
people[i] = people[i - 1];
}
}
int** reconstructQueue(int** people, int peopleSize, int* peopleColSize, int* returnSize, int** returnColumnSizes){
*returnSize = peopleSize;
*returnColumnSizes = (int* )malloc(sizeof(int) * peopleSize);
for (int i = 0; i < peopleSize; i++) {(*returnColumnSizes)[i] = 2;}
qsort(people, peopleSize, sizeof(int*), cmp);
//排序完以后遍历入队
for (int i = 0; i < peopleSize; i++)
{
//由k判断应该放在哪里
int pos = people[i][1];
int* tmp = people[i];
//需要插入则之后的位置直到他原先的所在前一个位置都要向后移位
move(people, peopleSize, pos, i);
people[pos] = tmp;
}
return people;
}
重叠区间
例题452(中等)用最少数量的箭引爆气球
注意要点:
- 应该能轻松的联想到,只要重叠的区间,我就只用一支箭来搞定;
- 为了求重叠区间,就需要对数组进行排序,头尾的排序都可以,为了简单就采取头部的排序;
- 只要重叠,箭的数量就不必自增;
- 为了方便,如果是重叠的,每一次还要把最小上界更新到当前i数组;
为了更简单的理解,我白嫖了代码随想录的示意图,后面的很多重叠区间的题目都可以套用这张图来思考:
下面贴出代码:
CPP版本
class Solution {
private:
static bool cmp(const vector<int>& a, const vector<int>& b)
{
return a[0] < b[0];
}
public:
int findMinArrowShots(vector<vector<int>>& points) {
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;
}
};
C版本
int cmp(const void* a, const void* b)
{
int* tmp1 = *(int** )a;
int* tmp2 = *(int** )b;
return tmp1[0] == tmp2[0] ? tmp1[1] > tmp2[1] : tmp1[0] > tmp2[0];
}
int findMinArrowShots(int** points, int pointsSize, int* pointsColSize){
if (!pointsSize) {return 0;}
//先按照第一个元素的大小从小到大排列
qsort(points, pointsSize, sizeof(int*), cmp);
int num = 1;
for (int i = 1; i < pointsSize; i++)
{
if (points[i][0] > points[i - 1][1])
{
num++; // 需要一支箭
}
else
{
// 更新重叠气球最小右边界
points[i][1] = points[i-1][1] < points[i][1] ? points[i-1][1] : points[i][1];
}
}
return num;
}
例题435(中等)无重叠区间
注意要点:
- 本题与上一题类似,就是求重叠区间;同样还是采用左边界的排序继续后续逻辑操作;
- 如果当前的头部>全局尾部,更新全局尾部为当前数组尾部即可;
- 否则就要更新重叠区间数量,再更新取更小尾部(以此来删去区间更大的数组,减少重叠区间的长度)。
下面贴出代码:
CPP版本
class Solution {
private:
static bool cmp(const vector<int>& a, const vector<int>& b)
{
return a[0] < b[0];
}
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), cmp);
int count = 0;
int end = intervals[0][1];
for (int i = 1; i < intervals.size(); i++)
{
if (intervals[i][0] >= end) {end = intervals[i][1];}
else
{
count++;
end = min(end, intervals[i][1]);
}
}
return count;
}
};
C版本
int cmp(const void* a, const void* b)
{
int* tmp1 = *(int** )a;
int* tmp2 = *(int** )b;
return tmp1[0] == tmp2[0] ? tmp1[1] > tmp2[1] : tmp1[0] > tmp2[0];
}
int eraseOverlapIntervals(int** intervals, int intervalsSize, int* intervalsColSize){
if (!intervalsSize) {return 0;}
qsort(intervals, intervalsSize, sizeof(int*), cmp);
int num = 0;
for (int i = 1; i < intervalsSize; i++)
{
if (intervals[i][0] < intervals[i - 1][1])
{
num++;
if (intervals[i][1] > intervals[i-1][1])
{
intervals[i][1] = intervals[i-1][1];
}
}
}
return num;
}
例题56(中等)合并区间
注意要点:
- 同样是重叠区间的判断,只不过这一题涉及重叠之后的合并;
- 可以直接比较当前result数组中的最后一个尾部元素,进而判断是否需要合并。
合并的逻辑如下图所示,这里再次把代码随想录的图片粘贴上来:
下面贴出代码:
CPP版本
class Solution {
private:
static bool cmp(const vector<int>& a, const vector<int>& b)
{
return a[0] < b[0];
}
public:
vector<vector<int>> merge(vector<vector<int>>& 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 (result.back()[1] >= intervals[i][0])
{
result.back()[1] = max(result.back()[1], intervals[i][1]);
}
else
{
result.push_back(intervals[i]);
}
}
return result;
}
};
C版本
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
int cmp(const void* a, const void* b)
{
//左边元素从小到大,如果相等,则右边元素从大到小
int* tmp1 = *(int** )a;
int* tmp2 = *(int** )b;
return tmp1[0] == tmp2[0] ? tmp1[1] < tmp2[1] : tmp1[0] > tmp2[0];
}
int** merge(int** intervals, int intervalsSize, int* intervalsColSize, int* returnSize, int** returnColumnSizes){
qsort(intervals, intervalsSize, sizeof(int*), cmp);
//开辟答案的二维数组,每列两个元素
int** ans = (int** )malloc(sizeof(int* ) * intervalsSize);
for (int i = 0; i < intervalsSize; i++)
{
ans[i] = (int* )malloc(sizeof(int) * 2);
}
*returnSize = 0;
ans[(*returnSize)++] = intervals[0];
for (int i = 1; i < intervalsSize; i++)
{
//发生重叠
if (intervals[i][0] <= ans[(*returnSize) - 1][1])
{
ans[(*returnSize) - 1][1] = fmax(ans[(*returnSize) - 1][1], intervals[i][1]);
}
else
{
ans[(*returnSize)++] = intervals[i];
}
}
*returnColumnSizes = (int* )malloc(sizeof(int) * (*returnSize));
for (int i = 0; i < *returnSize; i++)
{
(*returnColumnSizes)[i] = 2;
}
return ans;
}
划分字母区间
例题763(中等)划分字母区间
注意要点:
- 遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了;
- 借助哈希表从左往右遍历,把当前的位置作为hash的key存入,这样可以保证最后留在hash里的值就是最远位置;
- 再次遍历,判断是否达到最远位置,即可计算区间长度。
这一题我做过了一遍,第二遍还是忘了怎么做,把图贴在下面,更好理解和记忆这题的解题思路和流程:
下面贴出代码:
CPP版本
class Solution {
public:
vector<int> partitionLabels(string s) {
int hash[26] = {0};
for (int i = 0; i < s.size(); i++) {hash[s[i] - 'a'] = i;}
vector<int> result;
int left = 0, right = 0;
for (int i = 0; i < s.size(); i++)
{
right = max(right, hash[s[i] - 'a']);
if (i == right)
{
result.push_back(right - left + 1);
left = right + 1;
}
}
return result;
}
};
C版本
/**
1. Note: The returned array must be malloced, assume caller calls free().
*/
int* partitionLabels(char * s, int* returnSize){
int* hash = (int* )malloc(sizeof(int) * 26);
//遍历字符串,把当前位置放在对应的hash表中,最后就能得到这个字母最后一次出现的位置
for (int i = 0; i < strlen(s); i++)
{
hash[s[i] - 'a'] = i;
}
int* ans = (int* )malloc(sizeof(int) * strlen(s));
*returnSize = 0;
int left = 0, right = 0;
for (int i = 0; i < strlen(s); i++)
{
right = right > hash[s[i] - 'a'] ? right : hash[s[i] - 'a'];
if (i == right)
{
ans[(*returnSize)++] = right - left + 1;
left = right + 1;
}
}
return ans;
}
单调递增的数字
例题738(中等)单调递增的数字
注意要点:
- 从后往前遍历,可以把结果进行覆盖,而不用每次都判断然后全部修改;
- 贪心的体现在于,只要不满足条件,则低位直接变成9而高位-1;
- 需要一个标志位flag记录变9的位置。
下面贴出代码:
CPP版本
class Solution {
public:
int monotoneIncreasingDigits(int n) {
string strNum = to_string(n);
int flag = strNum.size();
for (int i = strNum.size() - 1; i > 0; i--)
{
if (strNum[i - 1] > strNum[i])
{
flag = i;
strNum[i - 1]--;
}
}
for (int i = flag; i < strNum.size(); i++) {strNum[i] = '9';}
return stoi(strNum);
}
};
C版本
int my_itoa(int n, char* s)
{
int count = 0;
while (n)
{
s[count++] = n % 10 + '0';
n /= 10;
}
s[count] = '\0';
for (int i = 0; i < count / 2; i++)
{
int tmp = s[i];
s[i] = s[count - i - 1];
s[count - i - 1] = tmp;
}
return count;
}
int monotoneIncreasingDigits(int n){
char* s = (char* )malloc(sizeof(char) * 11);
//字符串操作每一位的数字更简单
int len = my_itoa(n, s);
//printf("s[0] = %c\n", s[0]);
int flag = len;
for (int i = len - 1; i > 0; i--)
{
if (s[i - 1] > s[i])
{
flag = i;
s[i - 1]--;
}
}
for (int i = flag; i < len; i++)
{
s[i] = '9';
}
return atoi(s);
}
监控二叉树
例题968(困难)监控二叉树
注意要点:
- 根据题意,每一个节点分别有三种状态:该节点无覆盖、该节点有摄像头、该节点有覆盖;
- 根据这三种状态,从下往上遍历,通过子节点状态判定父节点应处于什么状态(自上而下到了叶子结点就可能多耗费摄像头);
- 如果子节点均覆盖,则当前节点一定为无覆盖;
- 如果两者之一有摄像头,那么该节点有覆盖;
- 如果两者之一无覆盖,那么该节点一定有摄像头;
- 最后需要注意递归完成后的头结点状态,如果是无覆盖状态,那么头结点就要再放一个摄像头;
- 需要注意,对无覆盖的状态判断应早于有摄像头的判断,这样才能够对该装摄像头的地方全部完成判断!
这道题比较难,主要是要对每个节点进行状态的分类加粗样式,同时逻辑推导两个孩子的状态应该对应的父亲的状态。
下面贴出代码:
CPP版本
/**
* 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;
// 一共有三种状态
// 0代表无覆盖,1代表有摄像头,2代表有覆盖
int traversal(TreeNode* cur)
{
if (!cur) {return 2;}
int left = traversal(cur->left);
int right = traversal(cur->right);
if (left == 2 && right == 2) {return 0;}
if (!left || !right)
{
result++;
return 1;
}
if (left == 1 || right == 1) {return 2;}
return -1;
}
public:
int minCameraCover(TreeNode* root) {
result = 0;
if (!traversal(root)) {result++;}
return result;
}
};
C版本
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* struct TreeNode *left;
* struct TreeNode *right;
* };
*/
int traversal(struct TreeNode* root, int* result)
{
//后续遍历
//0是无覆盖,1是有摄像头,2是有覆盖
if(!root) {return 2;} //空节点说明是叶子,不要摄像头且应该有覆盖到
int left = traversal(root->left, result);
int right = traversal(root->right, result);
//左右均覆盖,那此时这个节点肯定是无覆盖
if (left == 2 && right == 2) {return 0;}
//有一个无覆盖,另一个是有摄像头或有覆盖,这个节点应该有摄像头
if (!left || !right)
{
(*result)++;
return 1;
}
//左右节点有一个摄像头,那这个节点肯定有覆盖
if (left == 1 || right == 1) {return 2;}
return -1; //只是为了函数结构,实际不会进入这里
}
int minCameraCover(struct TreeNode* root){
int result = 0;
//还要考虑头结点,如果未覆盖需要+1
if (!traversal(root, &result)) {result++;}
return result;
}
总结
贪心法,比较找到解题套路,都是看着局部最优能推导出全局最优,且找不出反例,那就是贪心法解决问题了。
题目类型总结
贪心法可以解决比如之前说的发饼干、找零、取反的问题;同时也能解决序列的一些单调性相关问题;
贪心法还能解决股票投资问题(之后动态规划还会再讲);还有经典的区间重叠的各种问题,都是先对一个维度排序之后进行贪心;
困难的比如有两个相互影响的维度的贪心,这时一定要选择一边先贪心,然后再去考虑另一个维度的贪心情况!
最后我白嫖了代码随想录总结的贪心法问题以及一些注意要点,如下图所示:
一些函数API
406题中:vector的插入比较慢,就可以用list(底层是链表实现)
list<vector<int>> que;
for (int i = 0; i < people.size(); i++)
{
int pos = people[i][1];
list<vector<int>>:: iterator it = que.begin();
while (pos--) {it++;}
que.insert(it, people[i]);
}
同时,在C中的数组插入可以通过move然后赋值:
//需要插入则之后的位置直到他原先的所在前一个位置都要向后移位
move(people, peopleSize, pos, i);
people[pos] = tmp;
56题中,可以直接对vector的尾部元素进行提取进而比较是否需要重叠:
vector<vector<int>> result;
result.back()[1] >= intervals[i][0]
738题中,涉及字符串和整型数的相互转换:
string strNum = to_string(n);
stoi(strNum)