贪心算法
贪心算法一般分为如下四步:
将问题分解为若干个子问题 找出适合的贪心策略 求解每一个子问题的最优解 将局部最优解堆叠成全局最优解
这个四步其实过于理论化了,我们平时在做贪心类的题目 很难去按照这四步去思考,真是有点“鸡肋”。做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。
不好意思了,贪心没有套路,说白了就是常识性推导加上举反例。
好吧 开刷
455 分发饼干
思路不难但是注意读题啊。。。
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int gCount = 0, sCount = 0;
int gSize = g.length;
int sSize = s.length;
while (gSize > gCount && sSize > sCount) {
if (g[gCount] > s[sCount]) {
sCount++;
} else if (g[gCount] <= s[sCount]) {
sCount++;
gCount++;
}
}
return gCount;
}
}
注意java里对数组的排序和求值的方法
Arrays.sort();
s.length 数组没括号,list有括号length()
376 摆动序列
第一次没AC因为忽略了:
这题也可以用动态规划来解 等我刷到动态规划的。。再回来
于是考虑只在diff波动的时候才更新preDiff(有点绕)
class Solution {
public int wiggleMaxLength(int[] nums) {
int result = 1;
int preDiff = 0;
int currDiff = 0;
int n = nums.length;
for(int i=0;i<n-1;++i){
currDiff = nums[i+1]-nums[i];
if(preDiff<=0 && currDiff >0){
result++;
preDiff = currDiff;
}
else if(preDiff >=0 && currDiff<0){
result++;
preDiff = currDiff;
}
}
return result;
}
}
https://programmercarl.com/0376.摆动序列.html#思路
参考
53. 最大子序和
暴力法当然可以解,时间复杂度不满足。
贪心思路:
局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
class Solution {
public int maxSubArray(int[] nums) {
int curr = 0;
int result = Integer.MIN_VALUE;
int n = nums.length;
for (int i = 0; i < n; ++i) {
curr = curr + nums[i];
result = curr > result ? curr : result;
if (curr < 0) {
curr = 0;
continue;
}
}
return result;
}
}
注意java中最小量为Integer.MIN_VALUE
动态规划也可以解
122. 买卖股票的最佳时机 II
这道题算part2部分的了,但写都写了打算写个一发完的
因为可以多次买,贪心思路是把利润按天算入,当天买入第二天卖出,如果利润为正数就加到result中
class Solution {
public int maxProfit(int[] prices) {
int result = 0;
int n = prices.length;
for(int i = 1;i<n;++i){
if(prices[i]-prices[i-1] > 0){
result+=prices[i]-prices[i-1];
}
}
return result;
}
}
55. 跳跃游戏
感觉可以用回溯写一下:
class Solution {
public boolean result = false;
public boolean canJump(int[] nums) {
backTrace(1,nums);
return result;
}
public void backTrace(int curr, int[] nums) {
int n = nums.length;
if (result == true) {
return;
}
if (curr >= n) {
result = true;
return;
}
// if(plus = 0){
// return;
// }
int plus = nums[curr - 1];
for (int i = 1; i <= plus; ++i) {
curr += i;
backTrace(curr, nums);
curr -= i;
}
return;
}
}
但是有用例超时了。。感觉我这已经算剪过枝的了
于是看看贪心:
贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
这道题目关键点在于:不用拘泥于每次究竟跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。
注意这里更新覆盖范围,而不是更新每次跳到哪里,可以一步一步跳,在跳到最后一个格之前只要覆盖范围能达到,就可以直接返回true
class Solution {
public boolean canJump(int[] nums) {
int coverage = 0;
int n = nums.length;
if (n == 1) {
return true;
}
for (int i = 0; i <= coverage; ++i) {
coverage = Math.max(coverage, i + nums[i]);
if (coverage >= n-1) {
return true;
}
}
return false;
}
}
45.跳跃游戏 II (其实没太看懂)
感觉贪心算法可以是永远跳到coverage中最大的数上?
但每次跳完都要找到coverage中最大的数感觉时间复杂度有点高啊?
题解
从图中可以看出来,就是移动下标达到了当前覆盖的最远距离下标时,步数就要加一,来增加覆盖距离。最后的步数就是最少步数。
这里还是有个特殊情况需要考虑,当移动下标达到了当前覆盖的最远距离下标时
如果当前覆盖最远距离下标不是是集合终点,步数就加一,还需要继续走。
如果当前覆盖最远距离下标就是是集合终点,步数不用加一,因为不能再往后走了。
图中覆盖范围的意义在于,只要红色的区域,最多两步一定可以到!(不用管具体怎么跳,反正一定可以跳到)
核心思想:
i用来一步步模拟,ans才是实际走的步数
class Solution {
public int jump(int[] nums) {
int n = nums.length;
int resultStep = 0;
int currCoverage = 0;
int nextCoverage = 0;
for(int i= 0; i<n; ++i){//i其实是下一步要走的位置
nextCoverage = Math.max(i + nums[i], nextCoverage);
if(currCoverage >= n-1){
break;
}
if(i == currCoverage){
currCoverage = nextCoverage;
resultStep++;
}
}
return resultStep;
}
}
1005.K次取反后最大化的数组和
没思路 看题解:
第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
第二步:从前向后遍历,遇到负数将其变为正数,同时K–
第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
第四步:求和
注意java比较器:
为什么是 Math.abs(o2) - Math.abs(o1) 而不是 Math.abs(o1) - Math.abs(o2)?
在 Java 中,Comparator 接口的 compare 方法需要返回一个整数,用于表示两个对象的比较结果:
如果返回负数,表示第一个对象小于第二个对象。
如果返回零,表示两个对象相等。
如果返回正数,表示第一个对象大于第二个对象。
而且这里是从大到小的排序,如果2大于1,那么应该排在前面。而sorted() // 默认从小到大排序,所以应该反着来,返回正数表示第一个对象大于第二个对象,那么第一个对象就会被排到后面(而我们比较器的逻辑是第二个对象的绝对值大,要排在前面)此时逻辑正好是通顺的(反着来的)
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
nums = IntStream.of(nums).boxed()
.sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
.mapToInt(Integer::intValue).toArray();
int n = nums.length;
for (int i = 0; i < n; ++i) {
if (k == 0) {
break;
}
if (nums[i] < 0) {
nums[i] = -nums[i];
--k;
}
}
if (k % 2 == 1)
nums[n - 1] = -nums[n - 1];
return Arrays.stream(nums).sum();
}
}
顺便再写一下C++的比较器:
C++中的sort()正常也是从小到大排序
注意C++中sort的用法:要加begin和end
sort(A.begin(), A.end(), cmp);
C++中比较器要写成static
class Solution {
private:
static bool cmp(int a, int b) {
if (abs(a) > abs(b)) {
return true;
} else {
return false;
}
}
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) {
nums[i] = -nums[i];
--k;
}
if(k == 0){
break;
}
}
if (k % 2 != 0) {
nums[n - 1] = -nums[n - 1];
}
int sum = 0;
for (int a : nums) {
sum += a;
}
return sum;
}
};
134. 加油站
感觉暴力可解,但不知道怎么写贪心
看题解的贪心算法第二种(第一种感觉太怪了)
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int res = 0;
int sum = 0;
int start = 0;
int n = gas.size();
for (int i = 0; i < n; ++i) {
sum += gas[i]-cost[i];
res += gas[i]-cost[i];
if(res < 0){
start = i + 1;
res = 0;
}
}
if(sum < 0){
return -1;
}
return start;
}
};
JAVA这里也没什么坑 几乎是跟C++一样的写法
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int sum = 0;
int res = 0;
int n = gas.length;
int start = 0;
for(int i=0; i<n; ++i){
res += gas[i] - cost[i];
sum += gas[i] - cost[i];
if(res < 0){
res = 0;
start = i+1;
}
}
if(sum < 0){
return -1;
}
return start;
}
}
135 分发糖果
一开始想着更新i的时候同时更新i-1,后来举了反例 如果是递减的评分 那更新没完了 第一个小孩必须给最多,但一开始只能给一个,这就导致每次到新小孩的时候都要跑回去确认并更新前面小孩(直到第一个小孩的情况),这样肯定不行,时间复杂度太高了。
看题解,是一次只考虑左边小孩的,先是从前向后遍历一遍,再从后向前遍历一遍。。好聪明
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
int[] children = new int[n];
int sum = 0;
for (int i = 0; i < n; ++i) {
if (i == 0) {
children[i] = 1;
continue;
}
if (ratings[i] > ratings[i - 1]) {
children[i] = children[i - 1] + 1;
} else {
children[i] = 1;
}
}
for (int i = n - 1; i >= 0; --i) {
if (i == n - 1) {
continue;
}
if (ratings[i] > ratings[i + 1]) {
children[i] = Math.max(children[i + 1] + 1, children[i]);
}
}
for (int a : children) {
sum += a;
}
return sum;
}
}
860.柠檬水找零
easy题很好想,纯枚举就好了,顺便用一个数组记录当前零钱数
class Solution {
public boolean lemonadeChange(int[] bills) {
int[] charges = new int[3];
int n = bills.length;
for(int i =0; i < n; ++i){
if(bills[i] == 5){
charges[0]++;
continue;
}else if(bills[i] == 10){
if(charges[0] <= 0){
return false;
}else{
charges[0]--;
charges[1]++;
}
}else{//20 可以转 10+5 可以找 三个5
if(charges[0]>=1 && charges[1]>=1){
charges[2]++;
charges[0]--;
charges[1]--;
}else if(charges[0] >= 3){
charges[0] -= 3;
charges[2]++;
}else{
return false;
}
}
}
return true;
}
}
贪心的点在于,优先消耗10元的钱,在收入20的时候判断能否找零,先判断charges[0]>=1 && charges[1]>=1这一情况
406.根据身高重建队列
看了一下题解发现,这道题的两个维度需要分开来考虑,一次排列中不能既考虑h又考虑k
所以可以像分糖果题一样排列两次?
先排列身高,从大到小,相同身高的就排列第二个量,第二个量小的放前面(稍微举个例子就能明白为啥)
然后按照排列好的顺序往一个新数组里插入,插入位置就是people[i][1]的值。
注意vector中insert函数的用法
.insert(people.begin() + position, value), 第一个参数表示插入后将要成为的位置,第二个参数表示想要插入的值
class Solution {
private:
static bool cmp(vector<int> a, vector<int> b) {
if (a[0] > b[0]) {
return true;
} else if (a[0] == b[0]) {
if (a[1] > b[1]) {
return false;
} else {
return true;
}
}
return false;
}
public:
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
int n = people.size();
sort(people.begin(), people.end(), cmp); // 先按照身高从大到小排,
vector<vector<int>> result;
for (int i = 0; i < n; ++i) {
int position = people[i][1];
result.insert(result.begin() + position, people[i]);
}
return result;
}
};
数组的insert操作是O(n),因此这个方法时间复杂度是O(n^2)
Java 使用链表(LinkedList)进行插入操作
Linkedlist.add(index, value),将value插入到指定index里
最后使用toArray(new int[n][])将链表插入
当然也可以直接指定行和列数que.toArray(new int [n][2]);
452. 用最少数量的箭引爆气球
看题解:
就是先给气球排序,按气球最左端或最右端排序(需要自己写比较器)
排序后按气球个数遍历(不是按坐标遍历)
遍历过程中改变原数组:如果当前气球和上一个气球有重叠,就把当前气球的最右端缩小掉min(当前右端,上一个右端),否则就加一支箭。
题解里有个思路可以学习一下就是在遍历的时候会改变当前数组的值更方便条件的判断(我一般不习惯改变数组的值,会习惯性默认它不可变,但有的时候改一下会更方便)
注意:
注意题目中说的是:满足 xstart ≤ x ≤ xend,则该气球会被引爆。那么说明两个气球挨在一起不重叠也可以一起射爆,(注意的是if条件的判断要带等号)
class Solution {
private:
static bool cmp(vector<int> v1, vector<int> v2) {
if (v1[0] > v2[0]) {
return false;
}
return true;
}
public:
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]){
points[i][1] = min(points[i][1], points[i-1][1]);
}else{
result++;
}
}
return result;
}
};
435. 无重叠区间
贪心的思路是有重合就移最长的?(想了一下确实找不到反例?
写比较器的时候,按区间左侧排序,如果相等,右侧更长的放右边。
注意比较器放在private,必须是static bool
这是因为:由于sort ()为全局函数,因此它的比较函数cmp ()只能是静态函数或全局函数。 如果想在类class内重构cmp (),cmp ()的成分为非静态成员函数,需要依附于对象.cmp ()使用,导致无法在sort ()中调用cmp ()
(写了有用例不通过,盘了一下发现逻辑上确实是有点小问题但不打算抠了,直接看题解)
但题解的贪心没有考虑到丢弃最长的数组
题解可以按左侧排序,也可以按右侧排序,我这里先看按左侧排序的
为什么用第一个比较器的时候(cmp1)有些用例会报错?
class Solution {
private:
static bool cmp1(const vector<int>& a, const vector<int>& b){
if(a[0] > b[0]){
return false;
}else if (a[0] == b[0]){
if(a[1] > b[1]){
return false;
}
}
return true;
}
static bool cmp(const vector<int>& a, const vector<int>& b){
return a[0] < b[0];
}
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if(intervals.size() == 0){
return 0;
}
sort(intervals.begin(), intervals.end(), cmp);
int n = intervals.size();
int result = 0;
int end = intervals[0][1];
for(int i = 1; i < n; ++i){
if(intervals[i][0] < end){
result++;
end = min(end, intervals[i][1]);
}else{
end = intervals[i][1];
}
}
return result;
}
};
但是把第一个比较器加上等号就不会报错:
be like:
static bool cmp1(const vector<int>& a, const vector<int>& b){
if(a[0] >= b[0]){
return false;
}else if (a[0] == b[0]){
if(a[1] >= b[1]){
return false;
}
}
return true;
}
问了豆包:
似乎是在自建比较器的时候等于的逻辑也需要判断为false
763. 划分字母区间
完全没思路的一道题:
看题解:
在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。
那么思路就是维护一个数组记录当前字符的最远距离,需要遍历两遍
vector如何初始化?
一维数组,
vector v = {1,2,3,4};直接赋值
vector v(n), 初始化n个为0的值
vector v(n,m), 初始化n个为m的值
vector v(v0);
使用另一个数组来初始化,v0也必须是vector
int a[5] = {1,2,3,4,5};
vector b = {7,8,9,10};
vector va(a+1, a+4);
vector vb(b.begin()+1,b.end()-1);
可以用其他数组指针初始化vector
以上va为2,3,4
vb为8,9
unordered_map用法:(感觉太久不写了,基本语法有点忘记。。这里稍微总结一下)
insert({}),
insert(pair<string, int>(“banana”, 3));
insert(make_pair(“orange”, 7));
使用emplace()函数直接构造键值对并插入
data.emplace(“pear”, 4);
erase():从容器中删除一个键值对。
clear(),清空容器
find():查找具有指定键的元素,并返回指向该元素的迭代器。如果没有找到,则返回指向 end() 的迭代器。
count():返回具有指定键的元素数量。
class Solution {
public:
vector<int> partitionLabels(string s) {
int n = s.size();
vector<int> eachEnd;
unordered_map<char, int> eachPosition;
for (int i = 0; i < n; ++i) {
auto it = eachPosition.find(s[i]);
if (it == eachPosition.end()) {
eachPosition.insert({s[i], i});
} else {
it->second = i;
}
}
int each = 1;
int maxEnd = 0;
for (int i = 0; i < n; ++i) {
maxEnd = max(maxEnd, eachPosition.find(s[i])->second);
if (maxEnd == i) {
eachEnd.push_back(each);
each = 1;
} else {
each++;
}
}
return eachEnd;
}
};
注意这里,在第二次遍历的时候需要维护一个当前的最大边界的值,当i等于这个当前最大边界的值的时候就可以存了。
56. 合并区间
还是先排序(注意比较器的等号返回false)
需要维护一个新vector作为result返回
然后遍历,从1开始遍历,如果当前最左端大于新vector的最右端,pushback当前数组,else更新result中的当前数组的最右端到max(当前,result当前)。
按这个思路应该还比较好写的,时间复杂度O(nlogn),空间复杂度O(n)
class Solution {
private:
static bool cmp(vector<int> a, 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;
int n = intervals.size();
result.push_back(intervals[0]);
int curr = 0;
for (int i = 1; i < n; ++i) {
if (result[curr][1] < intervals[i][0]) {
result.push_back(intervals[i]);
++curr;
} else {
result[curr][1] = max(result[curr][1], intervals[i][1]);
}
}
return result;
}
};
738. 单调递增的数字
看题解;
还可以使用string strNum = to_string(N);
直接把int转为string
有点抽象,首先可以明确从个位开始遍历,就是从后向前遍历,
然后贪心的思路是,给能赋值的地方都赋值为9,最终视为能取到的最大
从个位开始遍历的时候,需要记录从哪一位开始到个位都赋值9。
如果当前位置的前一位大于当前位置,那么就记录前一位-1,当前赋值9
局部最优视为全局最优
很难想的一种贪心.
最后还用了一种语法糖直接把string转换为int了
return stoi(strNum);
int stoi(const string& str, size_t* idx = 0, int base = 10);
其中:
-
str:要转换的字符串。
-
idx:可选参数,用于存储转换结束的位置,即第一个无效字符的位置。
-
base:可选参数,指定转换时使用的进制,默认为10进制。
class Solution {
public:
int monotoneIncreasingDigits(int n) {
string result = to_string(n);
int flag = result.size();
for (int i = result.size() - 1; i > 0; --i) {
if (result[i - 1] > result[i]) {
flag = i;
result[i - 1]--;
}
}
for (int i = flag; i < result.size(); ++i) {
result[i] = '9';
}
return stoi(result);
}
};
968. 监控二叉树
后序遍历,需要从叶子节点开始遍历
枚举出每种节点的状态(共三种状态,有摄像头和没摄像头,没摄像头又可以分为有覆盖和无覆盖)
后序遍历递归地写,同时返回当前节点的状态
根据返回的左右节点状态判断当前节点应该处于啥状态
这里有个前提是空节点都返回有覆盖状态(不然叶子节点还需要加摄像头)
返回一共三种情况,
一种是左右节点都覆盖,此时当前节点应该作为被跳过的节点,不加摄像头,返回未覆盖的状态
第二种是左右节点存在没覆盖的,那当前节点一定返回加摄像头,同时计数
第三种是左右节点存在摄像头,那当前节点有覆盖。(注意第二种和第三种顺序,一定先筛掉子节点不存在无覆盖的情况再判断子节点是否有摄像头)
/**
* 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;
public:
int minCameraCover(TreeNode* root) {
if(traversal(root) == 0){
return ++result;
}
return result;
}
int traversal(TreeNode* root){//0为无覆盖,1为有摄像头,2为有覆盖
if(root == nullptr) return 2;
int left = traversal(root->left);
int right = traversal(root->right);
if(left == 0 || right == 0){
++result;
return 1;
}else if(left == 2 && right == 2){
return 0;
}else if(left == 1 || right == 1){
return 2;
}
return 0;
}
};
贪心算法算是告一段落(暂时)