算法-贪心

贪心法能够成立的条件是:通过局部最优能够得到全局最优

1. 分发饼干(LeetCode 455)

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;
并且每块饼干 j,都有一个尺寸 s[j] 。
如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。
你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

思路: 先对两个数组排序,用满足孩子的胃口的最小饼干进行投喂。

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g);
        Arrays.sort(s);
        int count = 0;
        for(int i = 0, j = 0; i<g.length && j<s.length; ++j) {
            if(s[j] >= g[i]) {
                count++;
                i++;
            }
        }
        return count;
    }
}

2. 摆动序列(LeetCode 376)

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

思路:将序列连成一条线,观察其上升和下降,取转折点(也就是摆动序列中的正负交替)作为子序列。

class Solution {
    // 观察序列的上升和下降,取波峰和波谷的转折点作为子序列
    public int wiggleMaxLength(int[] nums) {        
        int count = 1;
        int prediff = 0;
        int curdiff = 0;

        for(int i = 1; i<nums.length; ++i) {
            curdiff = nums[i] - nums[i-1];
            // 出现摆动的时候才更新prediff
            // 避免不减序列中出现连续相等的情况,如1 2 2 3
            if((prediff <= 0 && curdiff > 0) || (prediff >= 0 && curdiff < 0)) {
                count++;
                prediff = curdiff;
            }
        }
        return count;
    }
}

3. 最大子数组和(LeetCode 53)

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。

class Solution {
    public int maxSubArray(int[] nums) {
        int result = Integer.MIN_VALUE;
        int sum = 0;
        for(int i = 0; i<nums.length; ++i) {
            sum += nums[i];
            if(sum > result)
                result = sum;
            // 如果sum<0了,说明需要从下一个起点重新开始了
            if(sum < 0)
                sum = 0;
        }
        return result;
    }
}

4. 买卖股票的最佳时机II (LeetCode 122)

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。

思路:收集每天的正利润,累加作为最大利润。

class Solution {
    public int maxProfit(int[] prices) {
        int result = 0;
        int curdiff = 0;
        for(int i = 1; i<prices.length; ++i) {
            curdiff = prices[i] - prices[i-1];
            if(curdiff > 0) {
                result += curdiff;
            }
        }
        return result;
    }
}

5. 跳跃游戏(LeetCode 55)

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

思路:贪心法,每次更新最大覆盖范围,如果最大覆盖范围大于等于最后一个下标,则返回true。

class Solution {
    public boolean canJump(int[] nums) {
        // 最大覆盖范围
        int cover = nums[0];
        for(int i = 0; i<=cover; ++i) {
            // 更新最大覆盖距离
            if(i + nums[i] > cover)
                cover = i + nums[i];
            if(cover >= nums.length-1)
                return true;
        }
        return false;
    }
}

6. 跳跃游戏 II(LeetCode 45)

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i]
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。测试用例保证可以到达 nums[n - 1]

思路

  • 用双指针precovercurcover记录当前一步和下一步能够覆盖的最大距离
  • 如果i到达了precover,但还没有到数组的末尾,则需要再跳一步jump++
class Solution {
    public int jump(int[] nums) {
        if(nums.length == 1) return 0;
        int precover = nums[0];
        int curcover = precover;
        int jump = 1;
        for(int i = 0; i<=precover; ++i) {
            if(precover >= nums.length-1) {
                break;
            }
            if(i + nums[i] > curcover) {
                curcover = i + nums[i];
            }
            if(i == precover) {
                precover = curcover;
                jump++;
            }
        }
        return jump;
    }
}

7. K次取反后最大化的数组和(LeetCode 1005)

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:
选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。
重复这个过程恰好 k 次。可以多次选择同一个下标 i 。
以这种方式修改数组后,返回数组 可能的最大和 。

思路

  • 有负数的时候,先翻转绝对值大的负数
  • 当所有负数均翻转完成,仍有盈余翻转次数,则对最小的非负数进行翻转。
  • 盈余次数为偶数,则抵消;为奇数时,需要进行实质性的翻转
class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        int sum = 0;
        Arrays.sort(nums);
        for(int i = 0; i<nums.length && k>0; ++i) {
            if(nums[i] < 0) {
                nums[i] *= -1;
                k--;
            } 
            else    
                break;
        }

        // 所有的负数已经变为正数,还剩奇数次
        // 将当前最小的非负数取反
        if(k%2 == 1) {
            Arrays.sort(nums);
            nums[0] *= -1;
        }

        for(int i = 0; i<nums.length; ++i){
            sum += nums[i];
        }
        return sum;
    }
}

8. 加油站(LeetCode 134)

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组gascost,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。
如果存在解,则保证它是唯一的。

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        // 从当前start出发的累计剩余油量
        int curRest = 0;
        int totalRest = 0;
        int start = 0;
        for(int i = 0; i<gas.length; ++i) {
            totalRest += (gas[i] - cost[i]);
            curRest += (gas[i] - cost[i]);
            // 说明从之前的start出发不可行,至少要从i+1开始
            if(curRest < 0) {
                start = i+1;
                curRest = 0;
            }
        }
        // 不可能环绕一周
        if(totalRest < 0) 
            return -1;
        
        return start;
    }
}

9. 分发糖果(LeetCode 135)

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

思路

  • 一次遍历比较一种情况:左边>右边(正向遍历),右边>左边(反向遍历)
  • 从右向左遍历的中,取较大值,是为了同时满足该位置与左孩子、右孩子的大小关系
class Solution {
    public int candy(int[] ratings) {
        int sum = 0;
        int[] arr = new int[ratings.length];

        for(int i = 0; i<ratings.length; ++i) {
            arr[i] = 1;
        }
        
        // 右孩子比左孩子评分高,从左向右遍历
        for(int i = 1; i<ratings.length; ++i) {
            if(ratings[i] > ratings[i-1])
                arr[i] = arr[i-1] + 1;
        }

        // 左孩子比右孩子评分高,从右向左遍历
        for(int i = ratings.length - 2; i>=0; --i) {
            if(ratings[i] > ratings[i+1])
                // 取正向遍历和右孩子+1二者中的较大值
                arr[i] = Math.max(arr[i], arr[i+1] + 1);
        }

        for(int i = 0; i<ratings.length; ++i) {
            sum += arr[i];
        }
        
        return sum;
    }
}

10. 柠檬水找零(LeetCode 860)

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

class Solution {
    public boolean lemonadeChange(int[] bills) {
        // 分别代表5,10,20美元的数量
        int[] money = new int[3];
        for(int i = 0; i<money.length; ++i) {
            money[i] = 0;
        }

        for(int i = 0; i<bills.length; ++i) {
            if(bills[i] == 5){
                money[0]++;
            } 
            else if(bills[i] == 10) {
                if(money[0] > 0){
                    money[0]--; 
                    money[1]++;
                } 
                else 
                    return false;
            } 
            else {
                if(money[1] > 0 && money[0] > 0) {
                    money[2]++;
                    money[1]--;
                    money[0]--;
                } else if(money[0] >= 3) {
                    money[2]++;
                    money[0]-=3;
                }
                else 
                    return false;
            }
        }

        return true;
    }
}

11. 根据身高重建队列(LeetCode 406)(有难度)

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。
每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面正好有 ki 个身高大于或等于 hi 的人

请你重新构造并返回输入数组 people 所表示的队列。
返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

思路

  • 按照身高降序排列,然后的for循环类似于插牌,将根据k将每个元素插入到对应位置
  • 降序排列的目的:后面人的身高小于前面人,所以插入后不会破坏排在 前面人 之前的 大于等于其身高的人数
class Solution {
    public int[][] reconstructQueue(int[][] people) {
        LinkedList<int[]> que = new LinkedList<>();
        // 先按身高降序排列,若身高相同则按k升序排列
        Arrays.sort(people, (a, b) -> {
            if (a[0] == b[0]) return a[1] - b[1];
            return b[0] - a[0];
        });
        for(int i = 0; i<people.length; ++i) {
            // 参数1:插入位置;参数2:插入元素
            que.add(people[i][1], people[i]);
        }

        return que.toArray(new int[people.length][2]);
    }
}

12. 用最少的箭引爆气球(LeetCode 452)

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。
可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。

思路

  • 问题抽象:用多少个点可以覆盖所有的区间
  • 对于重叠的区间,只需要射出一只箭,箭的射出位置为重叠区间的最小右边界
class Solution {
    public int findMinArrowShots(int[][] points) {
        // points不为空,至少需要一只箭
        int count = 1;
        Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));

        for(int i = 1; i<points.length; ++i) {
            if(points[i][0] > points[i-1][1])
                count++;
            else 
                // 更新为重叠气球的最小右边界
                // 因为end大于这个值的、在当前重叠区间的气球,已经被引爆了
                points[i][1] = Math.min(points[i-1][1], points[i][1]);
        }
        return count;
    }
}

13. 删除区间(LeetCode 435)

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。

输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。

思路

  • 和上一题射箭的思路相似,保留重叠区间中的最小右边界
class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        int count = 0;
        Arrays.sort(intervals, (a,b)->Integer.compare(a[0], b[0]));
        for(int i = 1; i<intervals.length; ++i) {
            if(intervals[i][0] < intervals[i-1][1]) {
                count++;
                intervals[i][1] = Math.min(intervals[i-1][1], intervals[i][1]);
            }
        }
        return count;
    }
}

14. 划分字母区间(LeetCode 763)

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表

输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。 

思路

  • 用一个map,存储每个字母出现的最大数组下标
  • 注意结果收集的位置,这里的实现是在每次更新start前,以及for循环接收后收集最后一个区间
class Solution {
    public List<Integer> partitionLabels(String s) {
        // 用于存储每个字母最晚出现的下标
        int[] map = new int[26];
        for(int i = 0; i<s.length(); ++i) {
            map[s.charAt(i) - 'a'] = i;
        }

        List<Integer> result = new ArrayList<>();
        int start = 0;
        int end = map[s.charAt(0) - 'a'];
        for(int i = 0; i<s.length(); ++i) {
            if(i <= end) {
                if(map[s.charAt(i) - 'a'] > end){
                    end = map[s.charAt(i) - 'a'];
                }
            } else {
                result.add(end - start + 1);
                start = i;
                end = map[s.charAt(i) - 'a'];
            }
        }

        result.add(end - start + 1);
        return result;
    }
}

15. 合并区间(LeetCode 56)

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。
请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
class Solution {
    public int[][] merge(int[][] intervals) {
        List<int[]> result = new ArrayList<>();
        Arrays.sort(intervals, (a,b) -> Integer.compare(a[0], b[0]));
        int start = intervals[0][0];
        int end = intervals[0][1];
        for(int i = 1; i<intervals.length; ++i) {
            if(intervals[i][0] <= end) {
                end = Math.max(end, intervals[i][1]); // 取重叠区间的最大右边界
            } else {
                result.add(new int[]{start, end});
                start = intervals[i][0];
                end = intervals[i][1];
            }
        }

        result.add(new int[]{start, end});
        return result.toArray(new int[result.size()][2]);
    }
}

16. 单调递增的数字(LeetCode 738)

当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。

输入: n = 332
输出: 299
class Solution {
    public int monotoneIncreasingDigits(int n) {
        String s = String.valueOf(n);
        char[] chars = s.toCharArray();
        // 9开始的下标
        int nineStart = s.length();

        // 从后往前遍历
        for(int i = s.length() - 1; i>0; --i) {
        // 发现不符合递增的,将后一位置为9,前一位减一
            if(chars[i] < chars[i-1]) {
                chars[i-1]--;
                nineStart = i;
            }
        }
        // 将末尾的数字置为9
        for(int i = nineStart; i<s.length(); ++i) {
            chars[i] = '9';
        }

        return Integer.parseInt(String.valueOf(chars));
    }
}

17. 监控二叉树(LeetCode 968)(有难度)

给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。

思路

  • 从叶子节点的父节点开始设置摄像头,自底向上地设置(后序遍历)
  • 为每个节点维护一个状态state,分别表示该节点{有摄像头,被覆盖,未覆盖}
  • if-else的设置顺序需要仔细考虑
class Solution {
    int count = 0;
    public int minCameraCover(TreeNode root) {
        if(postOrderTraverse(root) == 3) 
            count++;
        
        return count;
    }

    public int postOrderTraverse(TreeNode root) {
        // 1表示有摄像头,2表示被覆盖,3表示未覆盖
        int state;
        if(root == null) {
            state = 2;
            return state;
        }

        int left = postOrderTraverse(root.left);
        int right = postOrderTraverse(root.right);
        
        if(left == 3 || right == 3) {
            // 5种情况 13,23,33,31,32
            state = 1;
            count++;
        } else if(left == 1 || right == 1){
            // 3种情况 11,12,21
            state = 2;
        } else {
            // 1种情况 22
            state = 3;
        }

        return state;
    }
}
posted @ 2024-08-13 11:25  Frank23  阅读(14)  评论(0编辑  收藏  举报