数据结构算法刷题--贪心算法

1. 贪心算法理论基础

2. 分发饼干

  • 题目:https://leetcode.cn/problems/assign-cookies/submissions/
  • 思路:
    • 贪心–局部最优可以得到全局最优
    • 优先考虑饼干,尽可能用小饼干满足小胃口
  • 代码实现:
// 贪心--局部最优可以得到全局最优
// 优先考虑饼干,尽可能用小饼干满足小胃口

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        // 1、先排序
        Arrays.sort(g);
        Arrays.sort(s);

        // 2、遍历饼干 -- 饼干从大到小去满足胃口
        int index = 0;
        int count = 0;
        for(int i = 0; i < s.length && index < g.length; ++i) {
            // 3、判断当前饼干能不能满足当前最小的胃口
            if(s[i] >= g[index]) {
                // 4、能满足 -- 胃口到下一个,去找能满足这个胃口的最小饼干
                ++index;
                ++count;
            }
        }

        return count;
    }
}

3. 摆动序列

  • 题目:https://leetcode.cn/problems/wiggle-subsequence/
  • 思路:动态规划
    • 第一个元素可以作为任意子序列的开始;
    • 后面的每一个元素都可以作为一个新的子序列的开始(初始化为1),或者追加到前面的元素后面作为一个波峰或者波谷(动态更新);
    • 遍历前面的元素,如果可以作为某个元素的波谷(小于前面的某个元素,然后删除之间的元素即可),一直比较当前作为波谷的最长子序列长度和作为最新前面某个元素的波谷后的最长子序列长度(那个元素作为波峰的最长子序列 + 1),动态规划使得其作为波谷的最长子序列值最大;
    • 如果可以作为某个元素的波峰,一直比较当前作为波峰的最长子序列和作为最新前面某个元素的波峰后的最长子序列长度,使得其作为波峰的最长子序列长度最大。
  • 代码实现:
// 动态规划

class Solution {
    public int wiggleMaxLength(int[] nums) {
        // 1、动态规划二维数组 dp[i][0] -- 第 i 个元素作为波峰;dp[i][1] -- 第 i 个元素作为波谷
        int[][] dp = new int[nums.length][2];

        // 2、第一个元素可以作为任意子序列的开始
        dp[0][0] = 1;
        dp[0][1] = 1;

        // 3、遍历,动态计算第 i 个位置作为子序列末尾的最长摆动子序列长度
        for(int i = 1; i < nums.length; ++i) {
            // 3.1 每一个元素都可以连接到前面的数字,或者作为一个子序列的开始
            dp[i][0] = 1;
            dp[i][1] = 1;

            // 3.2 索引 i 数字追加到前面的数字,遍历该节点是否可以作为波峰或波谷并更新
            for(int j = 0; j < i; ++j) {
                // 3.2.1 可以追加上去作为一个波谷
                if(nums[j] > nums[i]) {
                    dp[i][1] = Math.max(dp[i][1], dp[j][0] + 1);
                }

                // 3.2.2 可以追加上去作为一个波峰
                if(nums[j] < nums[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]);
    }
}

4. 最大子数组和

  • 题目:https://leetcode.cn/problems/maximum-subarray/
  • 思路:贪心算法:
    • 当前连续和为非负数时,就可以对后面的数的子序列和有正的贡献,保留累加 – 局部最优;
    • 在连续和为非负的过程中,一直统计当前的子数组和是否是新的最大值,更新给结果变量,最终得到全局最优;
    • 如果连续和为负数了,那么加上后面的数只会有负的贡献,从新开始计子序列就好了。
  • 代码实现:
// 贪心算法:局部最优:当当前连续子序列和为非负数时,一直和遍历的数相加 -- 这样即以当前数为子数组末尾的连续和最大

class Solution {
    public int maxSubArray(int[] nums) {
        int res = Integer.MIN_VALUE;
        int count = 0;
        for(int i = 0; i < nums.length; ++i) {
            // 实时更新加上当前数的结果 以及 最大结果
            count += nums[i];
            res = Math.max(res, count);

            // 判断加上这个数后 count 对后面是否还是正贡献
            if(count < 0) {
                // 负贡献 -- 直接重置 count
                count = 0;
            }
        }

        return res;
    }
}

5. 周末总结

6. 买卖股票的最佳时机

  • 题目:
  • 思路:
    • 贪心:局部最优:只在每天是正利润时候前一天买入后一天卖出;总体最优:获得最大利润
    • 关键点:连续多天的总利润可以拆解为每相邻两天利润的和
  • 代码实现:
// 贪心:局部最优:只在每天是正利润时候前一天买入后一天卖出;总体最优:获得最大利润;
// 关键点:连续多天的总利润可以拆解为每相邻两天利润的和

class Solution {
    public int maxProfit(int[] prices) {
        int res = 0;

        for(int i = 1; i < prices.length; ++i) {
            // 每一天相比前一天是正利润的情况下计算进去
            res += Math.max(prices[i] - prices[i - 1], 0);
        }

        return res;

    }
}

7. 跳跃游戏

  • 题目:https://leetcode.cn/problems/jump-game/
  • 思路:
    • 贪心:局部最优 – 每一次取最大跳跃范围,整体最优 – 最后得到最大的覆盖范围看是否可以包含最后一个位置
    • 每一个位置时计算可以从这里跳跃到的最大跳跃范围,遍历更新,当能覆盖最后一个位置就表明可以跳到
  • 代码实现:
// 贪心:局部最优 -- 每一次取最大跳跃范围,整体最优 -- 最后得到最大的覆盖范围看是否可以包含最后一个位置
// 每一个位置时计算可以从这里跳跃到的最大跳跃范围,遍历更新,当能覆盖最后一个位置就表明可以跳到

class Solution {
    public boolean canJump(int[] nums) {
        if(nums.length == 1) {
            return true;
        }

        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;
    }
}

8. 跳跃游戏 II

  • 题目:https://leetcode.cn/problems/jump-game-ii/
  • 思路:贪心算法:最小跳跃次数到达终点 – 每一步跳出去使整体能尽可能到达最远的地方
    • 每次在当前已经跳到的地方能到达的最大范围内遍历可以到达的点,使跳下一步以后整体能到达的距离最远
    • 如果最大范围覆盖了终点,那就是再跳一步就结束了
  • 代码实现:
// 贪心算法:最小跳跃次数到达终点 -- 每一步跳出去使整体能尽可能到达最远的地方
// 每次在当前已经跳到的地方能到达的最大范围内遍历可以到达的点,使跳下一步以后整体能到达的距离最远
// 当走到当前已经到的地方能到达的最大范围了,更新当前能到达的最远距离

class Solution {
    public int jump(int[] nums) {
        // 健壮性
        if(nums.length == 1) {
            return 0;
        }

        // 初始化变量
        // 跳的次数
        int count = 0;
        // 已经跳到的地方能到的最远距离
        int curCoverRange = 0;
        // 已经跳的地方再跳一步能到达的最远距离
        int totalCoverRange = 0;

        for(int i = 0; i < nums.length; ++i) {
            // 遍历更新再跳一步能到的最远距离
            totalCoverRange = Math.max(totalCoverRange, i + nums[i]);

            // 判断能不能到终点了
            if(totalCoverRange >= nums.length - 1) {
                ++count;
                break;
            }

            // 如果已经走到已经跳到的位置所能到达的最远距离,必须跳一步了
            if(i == curCoverRange) {
                ++count;
                curCoverRange = totalCoverRange;
            }
        }

        return count;
    }
}

9. K 次取反后最大化的数组和

  • 题目:https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/
  • 思路:
    • 贪心算法:有负数时,局部最优:将绝对值最大的负数优先取反,整体最优:数组和最大;没有了负数的时候,局部最优:将最小的自然数反复取反,整体最优:数组和最大。
    • 为了先操作负数,可以对数组排序,为了负数取反还有操作次数反复操作最小的自然数,需要按绝对值对数组排序;
    • 不排序思路 – 采用数组桶或者哈希表存放数组中元素的出现次数,然后先对负数取反,然后根据情况执行对取反后的最小自然数反复取反。
  • 代码实现:
// 贪心算法:
// 有负数时,局部最优:将绝对值最大的负数优先取反,整体最优:数组和最大;
// 没有了负数的时候,局部最优:将最小的自然数反复取反,整体最优:数组和最大。
// 可以先对数组排序,但是应该对绝对值进行排序,因为负数取反后有可能是最小的正数 -- 采用数组桶,替换排序

class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        // 1、数组桶,存放数组 -- -100 映射到索引 0
        int[] bucket = new int[201];
        for(int i = 0; i < nums.length; ++i) {
            ++bucket[nums[i] + 100];
        }

        // 2、利用 stream() 先统计目前的数组和
        int res = Arrays.stream(nums).sum();

        // 3、遍历小于 0 的元素,将其取反
        for(int i = 0; i < 100; ++i) {
            // 3.1 找到存在的负数
            if(bucket[i] > 0) {
                // 3.2 取对该负数取反的次数为该数出现的个数和剩余操作次数中的小者
                int operationCount = Math.min(bucket[i], k);
                // 3.3 取反 -- 更新数组和,由负到正 -- 和差两倍
                res += 2 * (-(i - 100)) * operationCount;
                // 3.4 取反 -- 更新负数的个数
                bucket[i] = bucket[i] - operationCount;
                // 3.5 取反 -- 更新取反后正数的个数
                bucket[200 - i] = bucket[200 - i] + operationCount;
                // 3.6 更新剩余操作次数
                k -= operationCount;
                // 3.7 判断是否还有取反操作次数,如果没有 break 掉小于 0 元素的遍历
                if(k == 0) {
                    break;
                }
            }
        }

        // 4、如果还有变换次数,反复取反绝对值最小的自然数
        // 如果剩余偶数次,变换后还是负数,不用;所以看变换次数是否是奇数
        if(k > 0 && k % 2 == 1) {
            for(int i = 100; i < 201; ++i) {
                // 4.1 找到数组中绝对值最小的数
                if(bucket[i] > 0) {
                    // 4.2 取反,更新结果
                    res -= 2 * (i - 100);
                    break;
                }
            }
        }

        return res;
    }
}

10. 周末总结

11. 加油站

  • 题目:https://leetcode.cn/problems/gas-station/
  • 思路:贪心算法
    • 局部最优 – 从索引 0 开始每一站油量的净剩余量累加和到 i 小于 0,起始位置至少从 i + 1 开始;
    • 全局最优 – 找到能跑完一圈的起始位置
  • 代码实现:
// 贪心算法:
// 局部最优 -- 从索引 0 开始每一站油量的净剩余量累加和到 i 小于 0,起始位置至少从 i + 1 开始;
// 全局最优 -- 找到能跑完一圈的起始位置

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        // 1、初始化变量
        int curSum = 0;
        int totalSum = 0;
        int index = 0;

        // 2、遍历,统计净油量累加剩余量
        for(int i = 0; i < gas.length; ++i) {
            // 2.1 统计剩余油量累加值
            curSum += gas[i] - cost[i];
            totalSum += gas[i] - cost[i];

            // 2.2 判断是否触发局部最优条件
            if(curSum < 0) {
                // 重置 curSum、index
                curSum = 0;
                index = i + 1;
            }
        }

        // 3、根据总的净油量剩余,判断是否可能绕环路一周
        if(totalSum < 0) {
            // 肯定不够跑完一圈
            return -1;
        }

        // 4、返回可以绕环路一周的起始位置
        return index;
    }
}

12. 分发糖果

  • 题目:https://leetcode.cn/problems/candy/submissions/
  • 思路:贪心算法;比较两次,每次只比较每个孩子一边的情况,两边一起比较很容易混乱;
    • 两次比较:一次从左往右遍历来确定右边比左边大时满足分发条件;另一次从右往左遍历来确定左边比右边评分大时去满足分发条件
    • 贪心:从左往右遍历,局部最优–只要右边比左边大,多一个糖果;全局最优 – 评分高的右边孩子一定比他左边孩子糖多;从右往左遍历,局部最优–左边比右边大,发的糖果是 比右边多一个和上面遍历中的大者;全局最优,得分高的比周围的糖多
  • 代码实现:
// 贪心算法;
// 遍历两次,一次从左往右遍历来确定右边比左边大时满足分发条件;
// 另一次从右往左遍历来确定左边比右边评分大时去满足分发条件
// 贪心:从左往右遍历,局部最优--只要右边比左边大,多一个糖果;全局最优 -- 评分高的右边孩子一定比他左边孩子糖多
// 从右往左遍历,局部最优--左边比右边大,发的糖果是 比右边多一个和上面遍历中的大者;全局最优,得分高的比周围的糖多
class Solution {
    public int candy(int[] ratings) {
        int[] candyDistribute = new int[ratings.length];
        candyDistribute[0] = 1;

        // 从左往右遍历
        for(int i = 1; i < ratings.length; ++i) {
            // 比较每个孩子是否比他左边的大
            candyDistribute[i] = ratings[i] > ratings[i - 1] ? candyDistribute[i - 1] + 1 : 1;
        } 

        // 从右往左遍历
        for(int i = ratings.length - 2; i >= 0; --i) {
            // 比较每个孩子得分是否比他右边的大
            candyDistribute[i] = Math.max(candyDistribute[i], ratings[i] > ratings[i + 1] ? candyDistribute[i + 1] + 1 : 1);
        }

        // 累加计算糖果数量
        int res = 0;
        for(int i = 0; i < candyDistribute.length; ++i) {
            res += candyDistribute[i];
        }

        return res;
    }
}

13. 柠檬水找零

  • 题目:https://leetcode.cn/problems/lemonade-change/
  • 思路:贪心,部最优 – 当收到20时,有10块的优先找一张10块的,因为5块的更万能;全局最优 – 尽可能都正确找零。
  • 代码实现:
// 贪心:局部最优 -- 当收到 20 时,优先找一张 10 块的,因为 5 块的更万能;全局最优 -- 尽可能都正确找零

class Solution {
    public boolean lemonadeChange(int[] bills) {
        int fiveBill = 0;
        int tenBill = 0;

        for(int i = 0; i < bills.length; ++i) {
            // 收钱、找零
            if(bills[i] == 5) {
                ++fiveBill;
            }
            if(bills[i] == 10) {
                --fiveBill;
                ++tenBill;
            }
            if(bills[i] == 20) {
                if(tenBill > 0) {
                    --tenBill;
                    --fiveBill;
                }
                else {
                    fiveBill -= 3;
                }
            }

            // 判断是否有无法找零的情况
            if(fiveBill < 0 || tenBill < 0) {
                return false;
            }
        }

        return true;
    }
}

14. 根据身高重建队列

  • 题目:https://leetcode.cn/problems/queue-reconstruction-by-height/
  • 思路:贪心算法,两个维度,同时考虑会顾此失彼,先考虑一个,再考虑另外一个 – 先按照身高进行降序排序,这样可以实现比某个身高高的在前面。身高排序之后:局部最优 – 优先按身高高的人的 k 属性插入,全局最优 – 排序满足队列要求
  • 代码实现:
// 贪心算法:两个维度,同时考虑会顾此失彼,先考虑一个,再考虑另外一个
// 先按照身高进行降序排序,这样可以实现比某个身高高的在前面
// 身高排序之后:局部最优 -- 优先按身高高的人的 k 属性插入,全局最优 -- 排序满足队列要求

class Solution {
    public int[][] reconstructQueue(int[][] people) {
        // 先按照身高维度降序排序
        Arrays.sort(people, (a, b) -> {
            if(a[0] == b[0]) {
                // 身高相同时按照 k 属性升序排列,确保有>=
                return a[1] - b[1];
            }
            // 按照身高降序
            return b[0] - a[0];
        });

        // 新建链表
        LinkedList<int[]> res = new LinkedList<>();

        // 遍历排序后的数组,优先按照身高高的人的 k 属性插入
        for(int i = 0; i < people.length; ++i) {
            res.add(people[i][1], people[i]);
        }

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

15. 周末总结

16. 根据身高体重重建队列(vector原理讲解)

17. 用最少数量的箭引爆气球

  • 题目:https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/
  • 思路:贪心算法:局部最优 – 每支箭射下来尽可能多的气球,全局最优 – 所需的弓箭数最少
    • 如何让一支箭射下来的气球尽可能多? 对气球按照起点排序以后,只要有重叠区域就只计一箭
    • 如何知道到哪里不是重叠区域了? 当新气球的起始位置大于前面若干个气球的右边界最小的就说明不重叠了
  • 代码实现:
// 贪心算法:局部最优 -- 每支箭射下来尽可能多的气球,全局最优 -- 所需的弓箭数最少
// 如何让一支箭射下来的气球尽可能多? 对气球按照起点排序以后,只要有重叠区域就只计一箭
// 如何知道到哪里不是重叠区域了? 当新气球的起始位置大于前面若干个气球的右边界最小的就说明不重叠了

class Solution {
    public int findMinArrowShots(int[][] points) {
        int count = 1;

        // 1、对气球按照左边界进行排序
        Arrays.sort(points, (p1, p2) -> Integer.compare(p1[0], p2[0]));

        // 2、遍历每个气球
        for(int i = 1; i < points.length; ++i) {
            // 2.1 判断当前是否仍和前面的气球有重叠区域,即判断当前左边界是否超过上一个右边界
            if(points[i][0] > points[i - 1][1]) {
                ++count;
            }
            else {
                // 2.2 如果不大于,更新右边界为当前重叠区域最小的右边界
                points[i][1] = Math.min(points[i][1], points[i - 1][1]);
            }
        }

        return count;
    }
}

18. 无重叠区间

  • 题目:https://leetcode.cn/problems/non-overlapping-intervals/
  • 思路:要求移除区间的最小数量,可以先找移除后剩余区间互不重叠的区间的个数,用总区间个数想减即可
    • 先对所有区间按照左边界进行排序,若干个区间有公共重叠区等效于他们中的最小右边界一直在这些区间左边界的右侧(代码中2.3)
    • 每个有公共重叠区的若干个区间最后只能保留一个,每跳出一个公共重叠去,互不重叠区间个数 + 1(代码中2.2)
  • 代码实现:
// 思路:要求移除区间的最小数量,可以先找移除后剩余区间互不重叠的区间的个数
// 先对所有区间按照左边界进行排序,若干个区间有公共重叠区等效于他们中的最小右边界一直在这些区间左边界的左侧
// 每个有公共重叠区的若干个区间最后只能保留一个,每跳出一个公共重叠去,互不重叠区间个数 + 1

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        // 1、先按照区间左边界升序排序
        Arrays.sort(intervals, (interval1, interval2) -> Integer.compare(interval1[0], interval2[0]));

        int count = 1;
        // 2、遍历排序后的区间
        for(int i = 1; i < intervals.length; ++i) {
            // 2.1 判断新的区间是否还是和前面的区间有公共重叠区
            if(intervals[i][0] >= intervals[i - 1][1]) {
                // 2.2 跳出了前面的公共重叠区,独立区间 + 1
                ++count;
            }
            else {
                // 2.3 还是和前面有公共重叠去,将当前右边界更新为公共重叠区最小的右边界
                intervals[i][1] = Math.min(intervals[i][1], intervals[i - 1][1]);
            }
        }

        return intervals.length - count;
    }
}

19. 划分字母区间

  • 题目:https://leetcode.cn/problems/partition-labels/
  • 思路:同一字母只能出现在一个片段中,那么每遍历一个片段时就要获得这里面字符最后出现的位置,当前片段至少要截取到那个位置;要划分为尽可能多的片段,那么当前片段就截取到已经在片段中字符到达的最远位置,不要再多截取
  • 代码实现:
// 思路:统计每个字符在字符串中最后出现的位置,然后从头开始遍历,记录遍历过程中字符能够到达的最远位置,然后如果当前位置和记录能到的的最远位置,那么就获得一个满足要求的子串,记录长度

class Solution {
    public List<Integer> partitionLabels(String s) {
        // 结果集合
        LinkedList<Integer> list = new LinkedList<>();
        // 记录当前片段中出现字符的最远位置,这个片段必须包含到那里
        int target = 0;
        // 记录当前片段的其实位置,用于计算长度
        int from = 0;

        // 1、记录字符串中每个字符出现的最远位置 -- 哈希
        // 1.1 哈希数组,索引代表字符,值代表位置
        int[] hash = new int[26];
        // 1.2 遍历 s,获得映射
        for(int i = 0; i < s.length(); ++i) {
            hash[s.charAt(i) - 'a'] = i;
        }

        // 2、从头遍历s
        for(int i = 0; i < s.length(); ++i) {
            // 2.1 一直更新当前走过片段出现字符能够到的最远距离,这是这个片段要到达的结束位置
            target = Math.max(target, hash[s.charAt(i) - 'a']);

            // 2.2 判断当前位置和 target 是否重合?
            if(i == target) {
                // 重合了,片段数量最多,就从这里截取,获得长度
                list.add(target + 1 - from);
                // 更新下一个片段的起点
                from = target + 1;
            }
        }

        return list;
    }
}

20. 合并区间

  • 题目:https://leetcode.cn/problems/merge-intervals/submissions/
  • 思路:对区间排完序以后,判断重叠区间。要用尽可能多少的大区间去覆盖多个重叠区间 – 那么一个新区间是否落在前面重叠区就看其左边界是否小于等于前面最大右边界
  • 代码实现:
// 思路:还是对区间排完序以后,判断重叠区间
// 现在还要用尽可能多少的大区间去覆盖多个重叠区间,那么一个新区间是否落在前面重叠区就看其左边界是否小于等于前面最大右边界

class Solution {
    public int[][] merge(int[][] intervals) {
        // 结果集合,最终需要转为数组
        List<int[]> res = new ArrayList<>();

        // 1、对区间按照左边界升序排序
        Arrays.sort(intervals, (interval1, interval2) -> Integer.compare(interval1[0], interval2[0]));
        // 一个重叠区间的起点
        int start = intervals[0][0];

        // 2、遍历每个区间
        for(int i = 1; i < intervals.length; ++i) {
            // 2.1 判断当前新区间是否落在前面的重叠区间
            if(intervals[i][0] > intervals[i - 1][1]) {
                // 不落在,将前面重叠区间首尾创建大区间加入结果
                res.add(new int[]{start, intervals[i - 1][1]});
                // 更新下一个重叠区间的起点、终点
                start = intervals[i][0];
            }
            else {
                // 落在,更新重叠区间的右边界为最大
                intervals[i][1] = Math.max(intervals[i][1], intervals[i - 1][1]);
            }
        }

        // 3、最后一个区间单独加入
        res.add(new int[]{start, intervals[intervals.length - 1][1]});

        return res.toArray(new int[res.size()][]);
    }
}

21. 贪心周末总结

贪心算法解决区间问题。

22. 单调递增的数字

  • 题目:https://leetcode.cn/problems/monotone-increasing-digits/
  • 思路:使数字单调递增,当遇到左边大于右边的,就让左边 - 1,右边变成9,然后逐个遍历
    • 遍历可以从左向右,也可以从右向左;如果从左向右,后面某一位的 - 1 可能让其再次小于它左边的,那么前面的再次需要改变;而从右往左可以一直保证右边的大于左边的(某个变为 9 时情况特殊)
    • 从右往左遍历过程中某个位置的变动可能使某位直接更新为 9,那么它右边的全部要变成9 --> 记录最左边变为 9 的那个数字
    • 为了方便取出 n 的每一位,将其转为字符串,再转为字符数组来取每一位
  • 代码实现:
// 思路:使数字单调递增,就是当遇到左边大于右边的,就让左边 - 1,右边变成9,然后逐个遍历
// 遍历可以从左向右,也可以从右向左;如果从左向右,后面某一位的 - 1 可能让其前面的再次需要改变;而从右往左可以一直保证右边的大于左边的
// 从右往左遍历过程中某个位置的变动可能使某位直接更新为 9,那么它右边的全部要变成9 --> 记录最左边变为 9 的那个数字
// 为了方便取出 n 的每一位,将其转为字符串,再转为字符数组来取每一位

class Solution {
    public int monotoneIncreasingDigits(int n) {
        // 1、将 n 转为字符数组
        String s = String.valueOf(n);
        char[] ch = s.toCharArray();

        // 记录从右往左第一个变为 9 的位置
        int flag = ch.length - 1;

        // 2、从后向前遍历
        for(int i = ch.length - 1; i > 0; --i) {
            // 2.1 判断当前位置左边的是否大于右边,一位数字大小比较可以直接利用其 ASCII 码
            if(ch[i - 1] > ch[i]) {
                // 2.2 大于,不单调递增,让左边 - 1,右边变成 9;更新变为 9 的位置
                ch[i] = '9';
                flag = i;
                --ch[i - 1];
            }
            // 2.3 单调递增,不用处理
        }

        // 3、将最左边的 9 之后的全部变为 9
        for(int i = flag + 1; i < ch.length; ++i) {
            ch[i] = '9';
        }

        // 4、转回 int
        return Integer.parseInt(new String(ch));
    }
}

23. 监控二叉树

  • 题目:https://leetcode.cn/problems/binary-tree-cameras/submissions/
  • 思路:贪心 – 如果叶子节点装摄像头比叶子节点不装摄像头而让其父节点装摄像头要多花费一层的覆盖,而头节点只有一个不用考虑这个摄像头的省与不省;=局部最优 – 叶子节点不放摄像头,然后从下往上遍历放摄像头;全局最优 – 摄像头数量最少=
    • 从下往上遍历? – 后续遍历,左右中
    • 如何判断一个节点是否需要装摄像头? – 先设定每个节点可以有三种状态:未被覆盖0,已被覆盖1,安装摄像头2
      • 1)如果一个节点的两个子节点都已被覆盖(状态11),那么应该让当前节点的父节点安装摄像头来覆盖它,它应该设定为未被覆盖;
      • 2)如果一个节点的两个子节点有一个未被覆盖(状态00,01,10,02,20),该节点必须安装摄像头;
      • 3)如果一个节点的子节点不存在未被覆盖情况,并且有任意一个安装了摄像头(状态21,12),当前节点为已被覆盖
    • 空节点问题,空节点应该被设置为哪种状态?叶子节点不安装,那么空节点不能是未被覆盖0;叶子节点是未覆盖以使其父节点安装摄像头,那么空节点不能是已安装2,所以空节点应当是已被覆盖1
  • 代码实现:
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */

 // 思路:贪心 -- 如果叶子节点装摄像头比叶子节点不装摄像头而让其父节点装摄像头可以省一层的覆盖,而头节点只有一个;局部最优 -- 叶子节点不放摄像头,然后从下往上遍历放摄像头
//  从下往上遍历? -- 后续遍历,左右中
// 如何判断一个节点是否需要装摄像头? -- 先设定每个节点可以有三种状态:未被覆盖0,已被覆盖1,安装摄像头2
// 1)如果一个节点的两个子节点都已被覆盖,那么应该让当前节点的父节点安装摄像头来覆盖它,它应该设定为未被覆盖;
// 2)如果一个节点的两个子节点有一个未被覆盖,该节点必须安装摄像头;
// 3)如果一个节点的子节点不存在未被覆盖情况,并且有任意一个安装了摄像头,当前节点为已被覆盖
// 空节点问题,空节点应该被设置为哪种状态?叶子节点不安装,那么空节点不能是未被覆盖0;叶子节点是未覆盖以使其父节点安装摄像头,那么空节点不能是已安装2,所以空节点应当是已被覆盖1

class Solution {
    private int count = 0;
    public int minCameraCover(TreeNode root) {
        // 调用递归遍历,同时判断根节点状态 -- 如果未被覆盖,这里必须装一个摄像头
        if(minCameraCoverMethod(root) == 0) {
            ++count;
        }

        return count;
    }

    // 递归遍历函数
    // 递归参数和返回值:参数,要遍历的节点;返回值:该节点的状态
    public int minCameraCoverMethod(TreeNode root) {
        // 递归终止条件 -- 空节点,返回一个已被覆盖状态 1
        if(root == null) {
            return 1;
        }

        // 本层逻辑 -- 遍历顺序:后序遍历,左右中
        // 左
        int leftStatus = minCameraCoverMethod(root.left);
        // 右
        int rightStatus = minCameraCoverMethod(root.right);
        // 中 -- 根据左右子节点的状态来设置其状态
        if(leftStatus == 1 && rightStatus == 1) {
            // 包含状态 11
            // 左右子节点都已经被覆盖 -- 设置其为未覆盖,以使其父节点被覆盖
            return 0;
        }
        else if(leftStatus == 0 || rightStatus == 0) {
            // 包含状态 00 01 10 02 20
            // 左右有任意一个未被覆盖,该节点必须装摄像头 -- count + 1
            ++count;
            return 2;
        }
        else {
            // 包含状态 12 21
            // 左右有任意一个安装了摄像头,且另一个已被筛选为至少被覆盖,当前节点设置为被覆盖
            return 1;
        }

    }
}

24. 贪心算法总结

你可能感兴趣的:(数据结构-算法刷题,java,贪心算法,算法)