文章目录
基础理论
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?每次拿最大就是局部最优
贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
上述过于理论化,在实际做题中,很难按照上面四部去逐步思考。
做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。
贪心没有套路,说白了就是常识性推导加上举反例。没有固定模板。
分发饼干
/**
* leetcode-455. 分发饼干
* @param g
* @param s
* @return
*/
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int ans = 0;
for (int i = 0;ans < s.length && i < g.length; i++) {
//如果当前饼干能满足当前孩子的胃口值,ans++,否则就继续查找更大的饼干
if(g[ans] <= s[i]){
ans++;
}
}
return ans;
}
摆动序列
/**
* leetcode-376. 摆动序列
* @param nums
* @return
*/
public int wiggleMaxLength(int[] nums) {
int ans = 1;
int preDiff = 0; //前一个差值
int curDiff = 0; //后一个差值
for (int i = 0; i < nums.length - 1; i++) {
curDiff = nums[i + 1] - nums[i];
// 摆动序列可以看作上坡下坡的过程 只有在坡度发生变化时需要更新preDiff
// 因为可能设计到平坡 即相同数字
if((preDiff <= 0 && curDiff >0) || (preDiff >=0 || curDiff <0)){
ans++; //总次数等于坡度变化次数+1
preDiff = curDiff;
}
}
return ans;
}
最大子数组和
/**
* 贪心,leetcode 53. 最大子数组和
* @param nums
* @return
*/
public int maxSubArray(int[] nums) {
// 只要连续和出现负数 立马从下一个元素重新开始记录最大值
int ans = Integer.MIN_VALUE;
int temp = 0;
for (int i = 0; i < nums.length; i++) {
temp += nums[i];
if (temp > ans) ans = temp;
if (temp <= 0) temp = 0;
}
return ans;
}
跳跃游戏
/**
* leetcode-55. 跳跃游戏
* @param nums
* @return
*/
public boolean canJump(int[] nums) {
// 3代表最多能跳3个格子
// 这三个格子作为起跳点都试一下 将能跳的最远距离不断更新
int cover = 0;
for (int i = 0; i <= cover; i++) {
cover = Math.max(cover, i + nums[i]); //这里跳最远指的是下标 不是长度 所以下面-1
if (cover >= nums.length - 1) return true;
}
return false;
}
跳跃游戏II
/**
* 45. 跳跃游戏 II
*
* @param nums
* @return
*/
public int jump(int[] nums) {
// 尽量往远了跳
int begin =0,end = 0;
int step = 0;
while (end < nums.length-1){
int temp = 0;
for (int i = begin; i <= end; i++) {
temp = Math.max(temp,i+nums[i]);
}
begin = end+1;
end = temp;
step++;
}
return step;
}
K 次取反后最大化的数组和
/**
* 1005. K 次取反后最大化的数组和
*
* @param nums
* @param k
* @return
*/
public int largestSumAfterKNegations2(int[] nums, int k) {
// 按照实际值大小排序没用 需要按照绝对值大小从大到小排序
// -2,9,9,8,4 k=5 实际值排序结果是24 正确答案是26
Integer[] integers = new Integer[nums.length];
for (int i = 0; i < nums.length; i++) {
integers[i] = nums[i];
}
Arrays.sort(integers, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Math.abs(o2) - Math.abs(o1); //降序
}
});
int ans = 0;
for (int i = 0; i < integers.length; i++) {
if (integers[i] < 0 && k > 0) { // 遇到负就变正
integers[i] = -integers[i];
k--;
}
}
if (k % 2 == 1) integers[integers.length - 1] *= -1; // k还没消耗完 就用最小的正值去消耗
for (int i = 0; i < integers.length; i++) {
ans += integers[i];
}
return ans;
}
public int largestSumAfterKNegations(int[] nums, int k) {
Arrays.sort(nums);
int ans = 0;
for (int i = 0; i < nums.length && k >0; i++) { //负数先抵消
if (nums[i] < 0) {
nums[i] = -nums[i];
k--;
}
}
// 负数可能抵消不完
Arrays.sort(nums);
//如果是奇数,选择排序后最小的那个数字,取反
if (k % 2 == 1) nums[0] *= -1;
for (int num : nums) {
ans += num;
}
return ans;
}
加油站
/**
* leetcode-134. 加油站
*
* @param gas
* @param cost
* @return
*/
public int canCompleteCircuit(int[] gas, int[] cost) {
int curRest = 0;
int totalRest = 0;
int ans = 0;
for (int i = 0; i < gas.length; i++) {
curRest += gas[i] - cost[i];
totalRest += gas[i] - cost[i];
if (curRest < 0) {
curRest = 0;
ans = (i + 1) % gas.length;
}
}
if (totalRest < 0)
return -1;
return ans;
}
分发糖果
/**
* leetcode-135. 分发糖果
* @param ratings
* @return
*/
public int candy(int[] ratings) {
// 测试数据 1 2 2 5 4 3 2
int[] ans = new int[ratings.length];
Arrays.fill(ans, 1);
for (int i = 1; i < ans.length; i++) { // 从前往后,确认右>左 符合规则
if (ratings[i] > ratings[i - 1]) {
ans[i] = ans[i - 1] + 1;
}
}
for (int i = ans.length - 2; i >= 0; i--) { // 从后往前 ,确认左>右 符合规则
if (ratings[i] > ratings[i + 1]) {
// 此时有两种选择 一种是其本身 一种是 ratings[i + 1]+1
// 取大的 这样既保证了大于左边(前面解决)也保证大于右边
Math.max( ans[i + 1] + 1,ans[i]);
}
}
int ret = 0;
for (int i = 0; i < ans.length; i++) {
ret += ans[i];
}
return ret;
}
柠檬水找零
/**
* leetcode-860. 柠檬水找零
*
* @param bills
* @return
*/
public boolean lemonadeChange(int[] bills) {
int five = 0;
int ten = 0;
if (bills[0] > 5) return false;
for (int i = 0; i < bills.length; i++) {
if (bills[i] == 5) five++;
else if (bills[i] == 10) {
ten++;
five--;
} else {
if (ten == 0) five -= 2; // 优先消耗10元
else ten--;
five--;
}
if (five < 0 || ten < 0) {
return false;
}
}
return true;
}
根据身高重建队列
/**
* leetcode-406. 根据身高重建队列
*
* @param people
* @return
*/
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) { // 身高从大到小排(身高相同k小的站前面)
if(o1[0] == o2[0]) return o1[1] - o2[1];
return o1[0] - o2[0];
}
});
LinkedList<int[]> que = new LinkedList<>();
for (int[] person : people) {
que.add(person[1],person);
}
return que.toArray(new int[que.size()][]);
}
用最少数量的箭引爆气球
/**
* 452. 用最少数量的箭引爆气球
* @param points
* @return
*/
public int findMinArrowShots(int[][] points) {
if(points.length == 0) return 0;
Arrays.sort(points, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return Integer.compare(o1[0],o2[0]); // 使用Integer内置方法 不会溢出
}
});
int ans = 1; // points 不为空至少需要一枝箭
for (int i = 1; i < points.length; i++) {
if(points[i][0] > points[i-1][1]){
// 第二只气球的左边界大于第一只气球的右边界 两气球不挨着
ans++;
}else {
// 更新重叠气球最小右边界
points[i][1] = Math.min(points[i][1],points[i-1][1]);
}
}
return ans;
}
无重叠区间
/**
* 435. 无重叠区间
*
* @param intervals
* @return
*/
public int eraseOverlapIntervals(int[][] intervals) {
// 本质上是一道预定会议问题
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[1] - o2[1]; // 按照结束时间排序
}
});
int right = intervals[0][1]; //有边界
int ans = 1;
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] > right) {
ans++;
right = intervals[i][1];
}
}
return intervals.length - ans;
}
划分字母区间
/**
* 763. 划分字母区间
*
* @param s
* @return
*/
public List<Integer> partitionLabels(String s) {
int[] map = new int[27];
List<Integer> list = new ArrayList<>();
for (int i = 0; i < s.length(); i++) { // 统计每一个字符最后出现的位置
map[s.charAt(i) - 'a'] = i;
}
int left = 0;
int right = 0; // 右边界
for (int i = 0; i < s.length(); i++) {
// 找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了
right = Math.max(right, map[s.charAt(i) - 'a']);
if (i == right) {
list.add(right - left + 1);
left = right + 1;
}
}
return list;
}
合并区间
/**
* 56. 合并区间
* @param intervals
* @return
*/
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[0] - o2[0];
}
});
int n = intervals.length;
// int[][] merge = new int[n][n];
List<int[]> list = new ArrayList<>();
int left = intervals[0][0];
int right = intervals[0][1];
for (int i = 1; i < n; i++) {
if (intervals[i][0] <= right) {
// 只需要更新右边界 左边界不需要更新
right = Math.max(right, intervals[i][1]);
} else {
list.add(new int[]{left, right});
left = intervals[i][0];
right = intervals[i][1];
}
}
list.add(new int[]{left, right});
return list.toArray(new int[list.size()][]);
}
单调递增的数字
/**
* 738. 单调递增的数字
*
* @param n
* @return
*/
public int monotoneIncreasingDigits(int n) {
char[] charArray = String.valueOf(n).toCharArray();
int start = charArray.length;
for (int i = charArray.length - 1; i > 0; i--) {
if (charArray[i] < charArray[i-1] ) {
charArray[i-1]-=1;
start = i+1;
}
}
for (int i = start; i < charArray.length; i++) {
charArray[i] = '9';
}
return Integer.parseInt(String.valueOf(charArray));
}
public int monotoneIncreasingDigits2(int n) {
int ones = 111111111; //本题中的n最大是9次方
int ans = 0; //累加最多不超过9次
for (int i = 0; i < 9; i++) {
while (ans + ones > n) {
ones /= 10;
}
ans += ones;
System.out.println(ones);
if (ones == 0) break;
}
return ans;
}
买卖股票的最佳时机含手续费
/**
* 714. 买卖股票的最佳时机含手续费
* @param prices
* @param fee
* @return
*/
public int maxProfit(int[] prices, int fee) {
int buy = prices[0] + fee;
int ans = 0;
for (int price : prices) { // 今天比前一天购买花费更少 更新
if (price + fee < buy) {
buy = price + fee;
}
if (price > buy) { //有利润可以获得 卖出
ans += price - buy;
buy = price;
//相当于一个后悔机制 后面遇到了比今天卖的价格更高的时候 选择价格高的卖出
// 减去buy相当于之前的price没有起作用,并且在之前的price中已经减去了成本(价格+交易费)
// 价格=前一天的buy(价格+交易费) 不做任何操作
}
}
return ans;
}
监控二叉树
int ans = 0;
/**
* leetcode-968. 监控二叉树
* @param root
* @return
*/
public int minCameraCover(TreeNode root) {
if (minCame(root) == 0){//若根节点未被覆盖
ans++;
}
return ans;
}
public int minCame(TreeNode root) {
// 定义三个状态
//0:该节点未被覆盖
//1:该节点有摄像头
//2:该节点被覆盖
// 后序遍历,根据左右节点的情况,来判读 自己的状态
if (root == null) {
// 空节点默认为 有覆盖状态,避免在叶子节点上放摄像头
return 2;
}
int left = minCame(root.left);
int right = minCame(root.right);
if (left == 2 && right == 2) {
// 如果左右节点都覆盖了的话, 那么本节点的状态就是无覆盖
return 0;
} else if (left == 0 || right == 0) { // 左右节点有一个未被覆盖
ans++;
return 1;
} else { // 左右节点至少有一个摄像头 处于被覆盖状态
return 2;
}
}