赞
踩
贪心一般解题步骤: 参见《代码随想录——什么时候用贪心》
贪心算法一般分为如下四步:
这个四步其实过于理论化了,我们平时在做贪心类的题目 很难去按照这四步去思考,真是有点“鸡肋”。
做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。
说实话贪心算法并没有固定的套路。
所以唯一的难点就是如何通过局部最优,推出整体最优。
那么如何能看出局部最优是否能推出整体最优呢?有没有什么固定策略或者套路呢?
不好意思,也没有! 靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。
有同学问了如何验证可不可以用贪心算法呢?
最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧。
可有有同学认为手动模拟,举例子得出的结论不靠谱,想要严格的数学证明。
一般数学证明有如下两种方法:
面试中基本不会让面试者现场证明贪心的合理性,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了。
举一个不太恰当的例子:我要用一下1+1 = 2,但我要先证明1+1 为什么等于2。严谨是严谨了,但没必要。
虽然这个例子很极端,但可以表达这么个意思:刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
例如刚刚举的拿钞票的例子,就是模拟一下每次拿做大的,最后就能拿到最多的钱,这还要数学证明的话,其实就不在算法面试的范围内了,可以看看专业的数学书籍!
所以这也是为什么很多同学通过(accept)了贪心的题目,但都不知道自己用了贪心算法,因为贪心有时候就是常识性的推导,所以会认为本应该就这么做!
那么刷题的时候什么时候真的需要数学推导呢?
例如这道题目:链表:环找到了,那入口呢? (opens new window),这道题不用数学推导一下,就找不出环的起始位置,想试一下就不知道怎么试,这种题目确实需要数学简单推导一下。
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
示例 2:
提示:
代码如下:
- class Solution {
- // 思路1:优先考虑饼干,小饼干先喂饱小胃口
- public int findContentChildren(int[] g, int[] s) {
- Arrays.sort(g);
- Arrays.sort(s);
- int start = 0;
- int count = 0;
- for (int i = 0; i < s.length && start < g.length; i++) {
- if (s[i] >= g[start]) {
- start++;
- count++;
- }
- }
- return count;
- }
- }
- class Solution {
- // 思路2:优先考虑胃口,先喂饱大胃口
- public int findContentChildren(int[] g, int[] s) {
- Arrays.sort(g);
- Arrays.sort(s);
- int count = 0;
- int start = s.length - 1;
- // 遍历胃口
- for (int index = g.length - 1; index >= 0; index--) {
- if(start >= 0 && g[index] <= s[start]) {
- start--;
- count++;
- }
- }
- return count;
- }
- }
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
示例 1:
示例 2:
示例 3:
代码如下:
- class Solution {
- public int wiggleMaxLength(int[] nums) {
- if (nums.length <= 1) {
- return nums.length;
- }
- //当前差值
- int curDiff = 0;
- //上一个差值
- int preDiff = 0;
- int count = 1;
- for (int i = 1; i < nums.length; i++) {
- //得到当前差值
- curDiff = nums[i] - nums[i - 1];
- //如果当前差值和上一个差值为一正一负
- //等于0的情况表示初始时的preDiff
- if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
- count++;
- preDiff = curDiff;
- }
- }
- return count;
- }
- }
- // DP
- class Solution {
- public int wiggleMaxLength(int[] nums) {
- // 0 i 作为波峰的最大长度
- // 1 i 作为波谷的最大长度
- int dp[][] = new int[nums.length][2];
-
- dp[0][0] = dp[0][1] = 1;
- for (int i = 1; i < nums.length; i++){
- //i 自己可以成为波峰或者波谷
- dp[i][0] = dp[i][1] = 1;
-
- for (int j = 0; j < i; j++){
- if (nums[j] > nums[i]){
- // i 是波谷
- dp[i][1] = Math.max(dp[i][1], dp[j][0] + 1);
- }
- if (nums[j] < nums[i]){
- // i 是波峰
- dp[i][0] = Math.max(dp[i][0], dp[j][1] + 1);
- }
- }
- }
-
- return Math.max(dp[nums.length - 1][0], dp[nums.length - 1][1]);
- }
- }
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
代码如下:
- class Solution {
- public int maxSubArray(int[] nums) {
- if (nums.length == 1){
- return nums[0];
- }
- int sum = Integer.MIN_VALUE;
- int count = 0;
- for (int i = 0; i < nums.length; i++){
- count += nums[i];
- sum = Math.max(sum, count); // 取区间累计的最大值(相当于不断确定最大子序终止位置)
- if (count <= 0){
- count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和
- }
- }
- return sum;
- }
- }
- // DP 方法
- class Solution {
- public int maxSubArray(int[] nums) {
- int ans = Integer.MIN_VALUE;
- int[] dp = new int[nums.length];
- dp[0] = nums[0];
- ans = dp[0];
-
- for (int i = 1; i < nums.length; i++){
- dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
- ans = Math.max(dp[i], ans);
- }
-
- return ans;
- }
- }
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
示例 2:
示例 3:
提示:
代码如下:
贪心:
- // 贪心思路
- class Solution {
- public int maxProfit(int[] prices) {
- int result = 0;
- for (int i = 1; i < prices.length; i++) {
- result += Math.max(prices[i] - prices[i - 1], 0);
- }
- return result;
- }
- }
动态规划:
- class Solution { // 动态规划
- public int maxProfit(int[] prices) {
- // [天数][是否持有股票]
- int[][] dp = new int[prices.length][2];
-
- // base case
- dp[0][0] = 0;
- dp[0][1] = -prices[0];
-
- for (int i = 1; i < prices.length; i++) {
- // dp公式
- dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
- dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
- }
-
- return dp[prices.length - 1][0];
- }
- }
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
示例 1:
示例 2:
代码如下:
- class Solution {
- public boolean canJump(int[] nums) {
- if (nums.length == 1) {
- return true;
- }
- //覆盖范围, 初始覆盖范围应该是0,因为下面的迭代是从下标0开始的
- int coverRange = 0;
- //在覆盖范围内更新最大的覆盖范围
- for (int i = 0; i <= coverRange; i++) {
- coverRange = Math.max(coverRange, i + nums[i]);
- if (coverRange >= nums.length - 1) {
- return true;
- }
- }
- return false;
- }
- }
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
示例:
说明: 假设你总是可以到达数组的最后一个位置。
代码如下:
- // 版本一
- class Solution {
- public int jump(int[] nums) {
- if (nums == null || nums.length == 0 || nums.length == 1) {
- return 0;
- }
- //记录跳跃的次数
- int count=0;
- //当前的覆盖最大区域
- int curDistance = 0;
- //最大的覆盖区域
- int maxDistance = 0;
- for (int i = 0; i < nums.length; i++) {
- //在可覆盖区域内更新最大的覆盖区域
- maxDistance = Math.max(maxDistance,i+nums[i]);
- //说明当前一步,再跳一步就到达了末尾
- if (maxDistance>=nums.length-1){
- count++;
- break;
- }
- //走到当前覆盖的最大区域时,更新下一步可达的最大区域
- if (i==curDistance){
- curDistance = maxDistance;
- count++;
- }
- }
- return count;
- }
- }
- // 版本二
- class Solution {
- public int jump(int[] nums) {
- int result = 0;
- // 当前覆盖的最远距离下标
- int end = 0;
- // 下一步覆盖的最远距离下标
- int temp = 0;
- for (int i = 0; i <= end && end < nums.length - 1; ++i) {
- temp = Math.max(temp, i + nums[i]);
- // 可达位置的改变次数就是跳跃次数
- if (i == end) {
- end = temp;
- result++;
- }
- }
- return result;
- }
- }
给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)
以这种方式修改数组后,返回数组可能的最大和。
示例 1:
示例 2:
示例 3:
提示:
代码如下:
- 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 len = nums.length;
- for (int i = 0; i < len; i++) {
- //从前向后遍历,遇到负数将其变为正数,同时K--
- if (nums[i] < 0 && K > 0) {
- nums[i] = -nums[i];
- K--;
- }
- }
- // 如果K还大于0,那么反复转变数值最小的元素,将K用完
-
- if (K % 2 == 1) nums[len - 1] = -nums[len - 1];
- return Arrays.stream(nums).sum();
-
- }
- }
在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
说明:
示例 1: 输入:
输出: 3 解释:
示例 2: 输入:
gas = [2,3,4]
cost = [3,4,3]
输出: -1
解释: 你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油。开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油。开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油。你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。因此,无论怎样,你都不可能绕环路行驶一周。
代码如下:
- // 解法1
- class Solution {
- public int canCompleteCircuit(int[] gas, int[] cost) {
- int sum = 0;
- int min = 0;
- for (int i = 0; i < gas.length; i++) {
- sum += (gas[i] - cost[i]);
- min = Math.min(sum, min);
- }
-
- if (sum < 0) return -1;
- if (min >= 0) return 0;
-
- for (int i = gas.length - 1; i > 0; i--) {
- min += (gas[i] - cost[i]);
- if (min >= 0) return i;
- }
-
- return -1;
- }
- }
- // 解法2
- class Solution {
- public int canCompleteCircuit(int[] gas, int[] cost) {
- int curSum = 0;
- int totalSum = 0;
- int index = 0;
- for (int i = 0; i < gas.length; i++) {
- curSum += gas[i] - cost[i];
- totalSum += gas[i] - cost[i];
- if (curSum < 0) {
- index = (i + 1) % gas.length ;
- curSum = 0;
- }
- }
- if (totalSum < 0) return -1;
- return index;
- }
- }
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:
那么这样下来,老师至少需要准备多少颗糖果呢?
示例 1:
示例 2:
代码如下:
- class Solution {
- /**
- 分两个阶段
- 1、起点下标1 从左往右,只要 右边 比 左边 大,右边的糖果=左边 + 1
- 2、起点下标 ratings.length - 2 从右往左, 只要左边 比 右边 大,此时 左边的糖果应该 取本身的糖果数(符合比它左边大) 和 右边糖果数 + 1 二者的最大值,这样才符合 它比它左边的大,也比它右边大
- */
- public int candy(int[] ratings) {
- int len = ratings.length;
- int[] candyVec = new int[len];
- candyVec[0] = 1;
- for (int i = 1; i < len; i++) {
- candyVec[i] = (ratings[i] > ratings[i - 1]) ? candyVec[i - 1] + 1 : 1;
- }
-
- for (int i = len - 2; i >= 0; i--) {
- if (ratings[i] > ratings[i + 1]) {
- candyVec[i] = Math.max(candyVec[i], candyVec[i + 1] + 1);
- }
- }
-
- int ans = 0;
- for (int num : candyVec) {
- ans += num;
- }
- return ans;
- }
- }
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。
顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
示例 1:
示例 2:
示例 3:
示例 4:
提示:
代码如下:
- class Solution {
- public boolean lemonadeChange(int[] bills) {
- int five = 0;
- int ten = 0;
-
- for (int i = 0; i < bills.length; i++) {
- if (bills[i] == 5) {
- five++;
- } else if (bills[i] == 10) {
- five--;
- ten++;
- } else if (bills[i] == 20) {
- if (ten > 0) {
- ten--;
- five--;
- } else {
- five -= 3;
- }
- }
- if (five < 0 || ten < 0) return false;
- }
-
- return true;
- }
- }
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
示例 1:
示例 2:
提示:
题目数据确保队列可以被重建
代码如下:
- class Solution {
- public int[][] reconstructQueue(int[][] people) {
- // 身高从大到小排(身高相同k小的站前面)
- Arrays.sort(people, (a, b) -> {
- if (a[0] == b[0]) return a[1] - b[1]; // a - b 是升序排列,故在a[0] == b[0]的狀況下,會根據k值升序排列
- return b[0] - a[0]; //b - a 是降序排列,在a[0] != b[0],的狀況會根據h值降序排列
- });
-
- LinkedList<int[]> que = new LinkedList<>();
-
- for (int[] p : people) {
- que.add(p[1],p); //Linkedlist.add(index, value),會將value插入到指定index裡。
- }
-
- return que.toArray(new int[people.length][]);
- }
- }
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
示例 1:
示例 2:
示例 3:
示例 4:
示例 5:
提示:
代码如下:
- /**
- * 时间复杂度 : O(NlogN) 排序需要 O(NlogN) 的复杂度
- * 空间复杂度 : O(logN) java所使用的内置函数用的是快速排序需要 logN 的空间
- */
- class Solution {
- public int findMinArrowShots(int[][] points) {
- // 根据气球直径的开始坐标从小到大排序
- // 使用Integer内置比较方法,不会溢出
- Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));
-
- int count = 1; // points 不为空至少需要一支箭
- for (int i = 1; i < points.length; i++) {
- if (points[i][0] > points[i - 1][1]) { // 气球i和气球i-1不挨着,注意这里不是>=
- count++; // 需要一支箭
- } else { // 气球i和气球i-1挨着
- points[i][1] = Math.min(points[i][1], points[i - 1][1]); // 更新重叠气球最小右边界
- }
- }
- return count;
- }
- }
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:
示例 2:
示例 3:
代码如下:
- class Solution {
- public int eraseOverlapIntervals(int[][] intervals) {
- Arrays.sort(intervals, (a,b)-> {
- return Integer.compare(a[0],b[0]);
- });
- int count = 1;
- for(int i = 1;i < intervals.length;i++){
- if(intervals[i][0] < intervals[i-1][1]){
- intervals[i][1] = Math.min(intervals[i - 1][1], intervals[i][1]);
- continue;
- }else{
- count++;
- }
- }
- return intervals.length - count;
- }
- }
方法二:按左边排序,不管右边顺序。相交的时候取最小的右边。
- class Solution {
- public int eraseOverlapIntervals(int[][] intervals) {
- Arrays.sort(intervals, (a,b)-> {
- return Integer.compare(a[0],b[0]);
- });
- int remove = 0;
- int pre = intervals[0][1];
- for(int i = 1; i < intervals.length; i++) {
- if(pre > intervals[i][0]) {
- remove++;
- pre = Math.min(pre, intervals[i][1]);
- }
- else pre = intervals[i][1];
- }
- return remove;
- }
- }
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
示例:
提示:
代码如下:
- class Solution {
- public List<Integer> partitionLabels(String S) {
- List<Integer> list = new LinkedList<>();
- int[] edge = new int[26];
- char[] chars = S.toCharArray();
- for (int i = 0; i < chars.length; i++) {
- edge[chars[i] - 'a'] = i;
- }
- int idx = 0;
- int last = -1;
- for (int i = 0; i < chars.length; i++) {
- idx = Math.max(idx,edge[chars[i] - 'a']);
- if (i == idx) {
- list.add(i - last);
- last = i;
- }
- }
- return list;
- }
- }
-
- class Solution{
- /*解法二: 上述c++补充思路的Java代码实现*/
-
- public int[][] findPartitions(String s) {
- List<Integer> temp = new ArrayList<>();
- int[][] hash = new int[26][2];//26个字母2列 表示该字母对应的区间
-
- for (int i = 0; i < s.length(); i++) {
- //更新字符c对应的位置i
- char c = s.charAt(i);
- if (hash[c - 'a'][0] == 0) hash[c - 'a'][0] = i;
-
- hash[c - 'a'][1] = i;
-
- //第一个元素区别对待一下
- hash[s.charAt(0) - 'a'][0] = 0;
- }
-
-
- List<List<Integer>> h = new LinkedList<>();
- //组装区间
- for (int i = 0; i < 26; i++) {
- //if (hash[i][0] != hash[i][1]) {
- temp.clear();
- temp.add(hash[i][0]);
- temp.add(hash[i][1]);
- //System.out.println(temp);
- h.add(new ArrayList<>(temp));
- // }
- }
- // System.out.println(h);
- // System.out.println(h.size());
- int[][] res = new int[h.size()][2];
- for (int i = 0; i < h.size(); i++) {
- List<Integer> list = h.get(i);
- res[i][0] = list.get(0);
- res[i][1] = list.get(1);
- }
-
- return res;
-
- }
-
- public List<Integer> partitionLabels(String s) {
- int[][] partitions = findPartitions(s);
- List<Integer> res = new ArrayList<>();
- Arrays.sort(partitions, (o1, o2) -> Integer.compare(o1[0], o2[0]));
- int right = partitions[0][1];
- int left = 0;
- for (int i = 0; i < partitions.length; i++) {
- if (partitions[i][0] > right) {
- //左边界大于右边界即可纪委一次分割
- res.add(right - left + 1);
- left = partitions[i][0];
- }
- right = Math.max(right, partitions[i][1]);
-
- }
- //最右端
- res.add(right - left + 1);
- return res;
-
- }
- }
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
示例 2:
代码如下:
- /**
- 时间复杂度 : O(NlogN) 排序需要O(NlogN)
- 空间复杂度 : O(logN) java 的内置排序是快速排序 需要 O(logN)空间
- */
- class Solution {
- public int[][] merge(int[][] intervals) {
- List<int[]> res = new LinkedList<>();
- //按照左边界排序
- Arrays.sort(intervals, (x, y) -> Integer.compare(x[0], y[0]));
- //initial start 是最小左边界
- int start = intervals[0][0];
- int rightmostRightBound = intervals[0][1];
- for (int i = 1; i < intervals.length; i++) {
- //如果左边界大于最大右边界
- if (intervals[i][0] > rightmostRightBound) {
- //加入区间 并且更新start
- res.add(new int[]{start, rightmostRightBound});
- start = intervals[i][0];
- rightmostRightBound = intervals[i][1];
- } else {
- //更新最大右边界
- rightmostRightBound = Math.max(rightmostRightBound, intervals[i][1]);
- }
- }
- res.add(new int[]{start, rightmostRightBound});
- return res.toArray(new int[res.size()][]);
- }
- }
- // 版本2
- class Solution {
- public int[][] merge(int[][] intervals) {
- LinkedList<int[]> res = new LinkedList<>();
- Arrays.sort(intervals, (o1, o2) -> Integer.compare(o1[0], o2[0]));
- res.add(intervals[0]);
- for (int i = 1; i < intervals.length; i++) {
- if (intervals[i][0] <= res.getLast()[1]) {
- int start = res.getLast()[0];
- int end = Math.max(intervals[i][1], res.getLast()[1]);
- res.removeLast();
- res.add(new int[]{start, end});
- }
- else {
- res.add(intervals[i]);
- }
- }
- return res.toArray(new int[res.size()][]);
- }
- }
给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。
(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)
示例 1:
示例 2:
示例 3:
说明: N 是在 [0, 10^9] 范围内的一个整数。
代码如下:
- 版本1
- class Solution {
- public int monotoneIncreasingDigits(int N) {
- String[] strings = (N + "").split("");
- int start = strings.length;
- for (int i = strings.length - 1; i > 0; i--) {
- if (Integer.parseInt(strings[i]) < Integer.parseInt(strings[i - 1])) {
- strings[i - 1] = (Integer.parseInt(strings[i - 1]) - 1) + "";
- start = i;
- }
- }
- for (int i = start; i < strings.length; i++) {
- strings[i] = "9";
- }
- return Integer.parseInt(String.join("",strings));
- }
- }
java版本1中创建了String数组,多次使用Integer.parseInt了方法,这导致不管是耗时还是空间占用都非常高,用时12ms,下面提供一个版本在char数组上原地修改,用时1ms的版本
- 版本2
- class Solution {
- public int monotoneIncreasingDigits(int n) {
- String s = String.valueOf(n);
- char[] chars = s.toCharArray();
- int start = s.length();
- for (int i = s.length() - 2; i >= 0; i--) {
- if (chars[i] > chars[i + 1]) {
- chars[i]--;
- start = i+1;
- }
- }
- for (int i = start; i < s.length(); i++) {
- chars[i] = '9';
- }
- return Integer.parseInt(String.valueOf(chars));
- }
- }
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例 1:
示例 2:
提示:
代码如下:
- class Solution {
- int res=0;
- public int minCameraCover(TreeNode root) {
- // 对根节点的状态做检验,防止根节点是无覆盖状态 .
- if(minCame(root)==0){
- res++;
- }
- return res;
- }
- /**
- 节点的状态值:
- 0 表示无覆盖
- 1 表示 有摄像头
- 2 表示有覆盖
- 后序遍历,根据左右节点的情况,来判读 自己的状态
- */
- public int minCame(TreeNode root){
- if(root==null){
- // 空节点默认为 有覆盖状态,避免在叶子节点上放摄像头
- return 2;
- }
- int left=minCame(root.left);
- int right=minCame(root.right);
-
- // 如果左右节点都覆盖了的话, 那么本节点的状态就应该是无覆盖,没有摄像头
- if(left==2&&right==2){
- //(2,2)
- return 0;
- }else if(left==0||right==0){
- // 左右节点都是无覆盖状态,那 根节点此时应该放一个摄像头
- // (0,0) (0,1) (0,2) (1,0) (2,0)
- // 状态值为 1 摄像头数 ++;
- res++;
- return 1;
- }else{
- // 左右节点的 状态为 (1,1) (1,2) (2,1) 也就是左右节点至少存在 1个摄像头,
- // 那么本节点就是处于被覆盖状态
- return 2;
- }
- }
- }
贪心系列的时候就说了,贪心系列并不打算严格的从简单到困难这么个顺序来讲解。
因为贪心的简单题可能往往过于简单甚至感觉不到贪心,如果我连续几天讲解简单的贪心,估计录友们一定会不耐烦了,会感觉贪心有啥好学的。
但贪心的难题又真的有点难,所以我是简单困难交错着讲的,这样大家就感觉难度适中,而且贪心也没有什么框架和套路,所以对刷题顺序要求没有那么高。
但在贪心系列,我发的题目难度会整体呈现一个阶梯状上升,细心的录友们应该有所体会。
在刚刚讲过的回溯系列中,大家可以发现我是严格按照框架难度顺序循序渐进讲解的,和贪心又不一样,因为回溯法如果题目顺序没选好,刷题效果会非常差!
同样回溯系列也不允许简单困难交替着来,因为前后题目都是有因果关系的,相信跟着刷过回溯系列的录友们都会明白我的良苦用心。
每个系列都有每个系列的特点,我都会根据特点有所调整,大家看我每天的推送的题目,都不是随便找一个到就推送的,都是先有整体规划,然后反复斟酌具体题目的结果。
那么在贪心总结篇里,我按难易程度以及题目类型大体归个类。
贪心大总结正式开始:
在贪心系列开篇词关于贪心算法,你该了解这些! (opens new window)中,我们就讲解了大家对贪心的普遍疑惑。
跟着一起刷题的录友们就会发现,贪心思路往往很巧妙,并不简单。
贪心无套路,也没有框架之类的,需要多看多练培养感觉才能想到贪心的思路。
Carl个人认为:如果找出局部最优并可以推出全局最优,就是贪心,如果局部最优都没找出来,就不是贪心,可能是单纯的模拟。(并不是权威解读,一家之辞哈)
但我们也不用过于强调什么题目是贪心,什么不是贪心,那就太学术了,毕竟学会解题就行了。
在做贪心题的过程中,如果再来一个数据证明,其实没有必要,手动模拟一下,如果找不出反例,就试试贪心。面试中,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了
就像是 要用一下 1 + 1 = 2,没有必要再证明一下 1 + 1 究竟为什么等于 2。(例子极端了点,但是这个道理)
相信大家读完关于贪心算法,你该了解这些! (opens new window),就对贪心有了一个基本的认识了。
以下三道题目就是简单题,大家会发现贪心感觉就是常识。是的,如下三道题目,就是靠常识,但我都具体分析了局部最优是什么,全局最优是什么,贪心也要贪的有理有据!
贪心中等题,靠常识可能就有点想不出来了。开始初现贪心算法的难度与巧妙之处。
大家都知道股票系列问题是动规的专长,其实用贪心也可以解决,而且还不止就这两道题目,但这两道比较典型,我就拿来单独说一说
在出现两个维度相互影响的情况时,两边一起考虑一定会顾此失彼,要先确定一个维度,再确定另一个一个维度。
在讲解本题的过程中,还强调了编程语言的重要性,模拟插队的时候,使用C++中的list(链表)替代了vector(动态数组),效率会高很多。
所以在贪心算法:根据身高重建队列(续集) (opens new window)详细讲解了,为什么用list(链表)更快!
大家也要掌握自己所用的编程语言,理解其内部实现机制,这样才能写出高效的算法!
这里的题目如果没有接触过,其实是很难想到的,甚至接触过,也一时想不出来,所以题目不要做一遍,要多练!
关于区间问题,大家应该印象深刻,有一周我们专门讲解的区间问题,各种覆盖各种去重。
贪心算法:最大子序和 (opens new window)其实是动态规划的题目,但贪心性能更优,很多同学也是第一次发现贪心能比动规更优的题目。
贪心算法:加油站 (opens new window)可能以为是一道模拟题,但就算模拟其实也不简单,需要把while用的很娴熟。但其实是可以使用贪心给时间复杂度降低一个数量级。
最后贪心系列压轴题目贪心算法:我要监控二叉树! (opens new window),不仅贪心的思路不好想,而且需要对二叉树的操作特别娴熟,这就是典型的交叉类难题了。
周总结里会对每周的题目中大家的疑问、相关难点或者笔误之类的进行复盘和总结。
如果大家发现文章哪里有问题,那么在周总结里或者文章评论区一定进行了修正,保证不会因为我的笔误或者理解问题而误导大家。
所以周总结一定要看!
在这十八道贪心经典题目中,大家可以发现在每一道题目的讲解中,我都是把什么是局部最优,和什么是全局最优说清楚。
这也是我认为判断这是一道贪心题目的依据,如果找不出局部最优,那可能就是一道模拟题。
贪心专题汇聚为一张图:
这个图是 代码随想录知识星球 (opens new window)成员:海螺人 (opens new window)所画
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。