贪心
和人的贪心一样,贪心算法就是让每一步总是按照某种指标选取最优。所以不是所有的贪心都能获得最后的答案,在我们使用贪心的时候,确保自己贪心算法的正确性。
在我们无计可施的时候,也可以想一下贪心的算法,获得部分的分数。
常见套路
- 按照某种规则排序,然后按照某种顺序选择
例题选讲
题意:
你是一个老板,想要凑够 n 元,现在有 1元,5元,10元的纸票,问最少用多少张纸票才能凑够n元。
示例:
输入: n = 11
输出: 2
解释:一张 10 元, 一张 1 元 就可以组成11元。
思路:
有 1元的存在,所以必定可以凑出来。
接下来就是如何减少纸票的数量了。
很明显的一个想法就是优先使用面值大的纸票,如果面值大的纸票不能用了,就换面值小的纸票。
public int solve(int n){
int ans = 0;
ans = n / 10; // 算出来可以由几张面值为10元的纸票组成。
n = n % 10; // 还剩下多少钱。
ans += n / 5; // 答案加上可以由几张面值为5元的纸票组成的数量
n = n % 5; // 还剩下多少钱
ans += n; // 答案加上可以由几张面值为1元的纸票组成的数量
return ans; // 返回答案
}
题意:
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是每个孩子最多只能给一块饼干。
对每个孩子 i
,都有一个胃口值 g[i]
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j
,都有一个尺寸 s[j]
。如果 s[j] >= g[i]
,我们可以将这个饼干 j
分配给孩子 i
,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例:
输入: g = [3,2,7,4,9], s = [3,5,4,6,8]
输出: 2
解释:
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
思路:
对于每一个孩子来说,如果有好几个大于他胃口的饼干,应该选哪一个饼干呢,
再想一想,就会发现要选择大于他胃口的饼干里面尺寸最小的那一个,这样尺寸大的就可以满足胃口大的孩子了。
这就是一个贪心的思想。
那么具体怎么实现这个算法呢,
对于每一个孩子,我都需要遍历一遍饼干数组,在剩余的饼干中找一个尺寸最小的且大于该孩子胃口的,
如果可以找到,答案就加一,该饼干的尺寸赋值为 0, 代表已经用过了。
所以个这个算法的时间复杂度是 O ( n 2 ) O(n^2) O(n2) 假设 n 为数组长度大的那一个。
class Solution {
public int findContentChildren(int[] g, int[] s) {
int ans = 0;
int mn = Integer.MAX_VALUE;
for (int val : g){ // 循环孩子数组
int tmp = mn,pos = -1; // 初始化饼干大小, 饼干的位置。
for (int i = 0; i < s.length; ++i){ // 循环饼干数组
if (s[i] >= val){ // 饼干尺寸大于孩子胃口
if (s[i] <= tmp) { // 找 饼干尺寸最小的
tmp = s[i]; // 记录饼干尺寸
pos = i; // 记录饼干的位置
}
}
}
if (pos != -1){ // 代表找到了对于的饼干位置
ans++; // 答案 加一
s[pos] = 0; // 饼干的尺寸变成 0, 代表不用再用了。
}
}
return ans;
}
}
到这里就可以解决问题了,我们还可以用更短的时间复杂度嘛?
考虑到孩子的位置,饼干的位置是没有影响的,所以我们可以进行排序。
如果我们把 s 数组, g 数组排序之后再考虑这个问题!!!
按照胃口从小到大的顺序枚举每个孩子,
对于第 i 个孩子,我从小到大枚举饼干尺寸,找到第 j 个位置的饼干正好大于该孩子的胃口,我就给第 i 个孩子匹配第 j 个饼干。
对于第 i + 1 个孩子, j 之前的饼干尺寸都不满足条件,j 也被用过了,所以我只要从 j + 1 开始枚举就可以了。
每个孩子只会被枚举一次, 每个饼干也只会被枚举一次,加上排序的时间复杂度。
所以总的时间复杂度就是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),(这里省略了,具体的复杂度大家可以算一下)
class Solution {
public int findContentChildren(int[] g, int[] s) {
int ans = 0; // 最终答案
int index = 0; // 用来枚举饼干的位置
Arrays.sort(g);
Arrays.sort(s);
for(int i = 0; i < g.length; ++i){
while(index < s.length && s[index] < g[i]) index++; // 找第一个大于等于胃口的饼干位置。
if (index < s.length){ // 说明找到了对应的饼干
ans++;
index++; // 这个位置饼干被用了, 位置加一
}
}
return ans;
}
}
题意:
给你一个整数数组 arr
和一个整数 k
,其中数组长度是偶数,值为 n
。
现在需要把数组恰好分成 n / 2
对,以使每对数字的和都能够被 k
整除。
如果存在这样的分法,请返回 True
;否则,返回 False
。
示例:
输入:arr = [-1,1,-2,2,-3,3,-4,4], k = 3
输出:true
思路:
- 如果选
-1
另外一个数可以选择1
4
7
, 这些数有哪些性质呢? 它都是3
的倍数+1
, 我们把所有的数都映射到0 ~ n-1
, 具体选择哪一个就不重要了。 - 把所有的数
%k
,会得到一个数,但是这个数在-k+1 ~ k-1
之间,再次把这个数+k % k
, 就可以变成0 ~ k-1
了。 - 统计每个数出现的次数。对于 i 这个数, 想要组成 k, 另外一个数必然是
k-i
,所以只要比较一下两个数的个数是不是一样就可以了。 - 最后特判 0 出现的次数是不是偶数。因为 0 只能和 0 组合在一起。
public boolean canArrange(int[] arr, int k) {
int[] c = new int[k+10];
for (int i = 0; i < arr.length; ++i) {
arr[i] = (arr[i] % k + k) % k;
c[arr[i]]++; // 统计每个数出现的次数。
}
boolean vis = true;
if ((c[0] & 1) == 1) vis = false; // 特判 0 的个数
for (int i = 1; i < k; ++i)
if (c[i] != c[k-i]) vis = false; // 判断两个数的个数是不是一样的。
return vis;
}