12-周赛338总结

12-周赛338总结

前三题做的很快,第三题先超时了一次又因为数据WA了一次,看到第四题,果断放弃,然后恶补了一下拓扑排序

最近事情好多,加油!!

K 件物品的最大和【LC2600】

袋子中装有一些物品,每个物品上都标记着数字 10-1

给你四个非负整数 numOnesnumZerosnumNegOnesk

袋子最初包含:

  • numOnes 件标记为 1 的物品。
  • numZeroes 件标记为 0 的物品。
  • numNegOnes 件标记为 -1 的物品。

现计划从这些物品中恰好选出 k 件物品。返回所有可行方案中,物品上所标记数字之和的最大值。

  • 思路:贪心

    每次优先取数字较大的数字,取完了再取次大的,保证最终数字之和最大

  • 实现

    class Solution {
        public int kItemsWithMaximumSum(int numOnes, int numZeros, int numNegOnes, int k) {
            if (k <= numOnes){
                return k;
            }else if (k > numOnes && k <= numOnes + numZeros){
                return numOnes;
            }else{
                return numOnes - (k - numOnes - numZeros);
            }
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( 1 ) O(1) O(1)
      • 空间复杂度: O ( 1 ) O(1) O(1)

质数减法运算【LC2601】

给你一个下标从 0 开始的整数数组 nums ,数组长度为 n

你可以执行无限次下述运算:

  • 选择一个之前未选过的下标 i ,并选择一个 严格小于 nums[i] 的质数 p ,从 nums[i] 中减去 p

如果你能通过上述运算使得 nums 成为严格递增数组,则返回 true ;否则返回 false

严格递增数组 中的每个元素都严格大于其前面的元素

倒序遍历

  • 思路:贪心+枚举

    倒序遍历数组,如果 n u m s [ i ] ≥ n u m s [ i + 1 ] nums[i]\ge nums[i+1] nums[i]nums[i+1],那么需要将 n u m s [ i ] nums[i] nums[i]减去一个质数 p p p使数组满足严格递增。

    • 为了使整个数组都严格递增,我们应该使 p p p尽可能小,使 n u m s [ i ] nums[i] nums[i]尽可能大。【贪心】
    • n u m s [ i ] < n u m s [ i + 1 ] nums[i]nums[i]<nums[i+1]时,需要减去的最小值为 n u m s [ i ] − ( n u m s [ i + 1 ] − 1 ) nums[i] - (nums[i+1] - 1) nums[i](nums[i+1]1),然后我们可以从该值开始找到最小的质数,作为 p p p值,如果无法找到符合条件的 p p p值,那么返回false
  • 实现

    class Solution {
        public boolean primeSubOperation(int[] nums) {
            int n = nums.length;
            for (int i = n - 2; i >= 0; i--){
                if (nums[i] >= nums[i + 1]){// nums[i]需要至少减小至 nums[i+1] - 1 nums[i] - (nums[i+1] - 1)
                    boolean flag = false;
                    for (int j = nums[i] - (nums[i+1] - 1); j < nums[i]; j++){
                        if (isPrime(j)){
                            nums[i] -= j;
                            flag = true;
                            break;
                        }
                    }
                    if (!flag) return false;
                }
            }
            return true;
        }
        public boolean isPrime(int num){
            if (num <= 1) return false;
            for (int i = 2; i < num; i++){
                if (num % i == 0){
                    return false;
                }
            }
            return true;
        }
    }
    
    • 复杂度
      • 时间复杂度: O ( n C ) O(nC) O(nC)
      • 空间复杂度: O ( 1 ) O(1) O(1)

正序遍历

  • 思路

    倒序遍历是保持增序的同时尽可能保留最大值,那么正序遍历是保持增序的同时尽可能将值减小

  • 实现:二分查找

    假设前一个数字为 n u m s [ i − 1 ] nums[i-1] nums[i1],当前数组为 n u m s [ i ] nums[i] nums[i],设 p p p是使 n u m s [ i ] − p > n u m s [ i − 1 ] nums[i]-p \gt nums[i-1] nums[i]p>nums[i1]的最大质数,即 p p p是小于 n u m s [ i ] − n u m s [ i − 1 ] nums[i]-nums[i-1] nums[i]nums[i1]的最大质数,那么我们可以预处理质数列表,然后使用二分查找得到最大质数

    class Solution {
        private final static int MX = 1000;
        private final static int[] primes = new int[169];
    
        static {
            var np = new boolean[MX + 1];
            int pi = 1; // primes[0] = 0 避免二分越界
            for (int i = 2; i <= MX; ++i)
                if (!np[i]) {
                    primes[pi++] = i; // 预处理质数列表
                    for (int j = i; j <= MX / i; ++j)
                        np[i * j] = true;
                }
        }
    
        public boolean primeSubOperation(int[] nums) {
            int pre = 0;
            for (int x : nums) {
                if (x <= pre) return false;
                int j = lowerBound(primes, x - pre);
                pre = x - primes[j - 1];
            }
            return true;
        }
    
        // 见 https://www.bilibili.com/video/BV1AP41137w7/
        private int lowerBound(int[] nums, int target) {
            int left = -1, right = nums.length; // 开区间 (left, right)
            while (left + 1 < right) { // 区间不为空
                // 循环不变量:
                // nums[left] < target
                // nums[right] >= target
                int mid = left + (right - left) / 2;
                if (nums[mid] < target)
                    left = mid; // 范围缩小到 (mid, right)
                else
                    right = mid; // 范围缩小到 (left, mid)
            }
            return right;
        }
    }
    
    作者:灵茶山艾府
    链接:https://leetcode.cn/problems/prime-subtraction-operation/solutions/2191560/jian-ji-xie-fa-shai-zhi-shu-er-fen-cha-z-wj7i/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    
    • 复杂度
      • 时间复杂度: O ( n l o g U ) O(nlogU) O(nlogU), U U U是1000以内的质数个数
      • 空间复杂度: O ( 1 ) O(1) O(1)

使数组元素全部相等的最少操作次数【LC2602】

给你一个正整数数组 nums

同时给你一个长度为 m 的整数数组 queries 。第 i 个查询中,你需要将 nums 中所有元素变成 queries[i] 。你可以执行以下操作 任意 次:

  • 将数组里一个元素 增大 或者 减小 1

请你返回一个长度为 m 的数组 answer ,其中 answer[i]是将 nums 中所有元素变成 queries[i]最少 操作次数。

注意,每次查询后,数组变回最开始的值。

  • 思路:排序+二分查找+前缀和

    首先,大于 q q q的数字都需要减小至 q q q,小于 q q q的数字都需要增大至 q q q,而数组值都变为 q q q后的和固定为 q ∗ n q*n qn,因此我们可以先将数组排序,然后使用二分查找找到小于等于 q q q的最大值的下标,此时我们就将数组分为大于 q q q和小于 q q q的两堆,然后通过前缀和数组快速计算出这两堆的和,将其分别与都为 q q q的和相比就可以得到需要的操作数

  • 实现

    就是说,注意越界

    class Solution {
        public List<Long> minOperations(int[] nums, int[] queries) {
            List<Long> res = new ArrayList<>();
            Long sum = 0L;
            int n = nums.length;
            long[] preSum = new long[n + 1];
            Arrays.sort(nums);
            for (int i = 0; i < n; i++){
                preSum[i + 1] = preSum[i] + nums[i];
            }
            for (int q : queries){
                int index = binarySearch(nums, q);
                long low = preSum[index + 1];
                long high = preSum[n] - preSum[index + 1];
                long count1 = (long)q * (index + 1) - low;
                long count2 = high - (long)q * (n - index - 1);
                res.add(count1 + count2);
            }
            return res;
        }
        public int binarySearch(int[] nums, int target){
            int l = 0, r = nums.length - 1;
            while (l <= r){
                int mid = (l + r) >> 1;
                if (nums[mid] <= target){
                    l = mid + 1;
                }else{
                    r = mid - 1;
                }
            }
            return r;
        }
    }
    
    • 复杂度

      • 时间复杂度: O ( m l o g n ) O(mlogn) O(mlogn),n为数组长度,m为查询长度

      • 空间复杂度: O ( n ) O(n) O(n)

收集树中金币【LC2603】

给你一个 n 个节点的无向无根树,节点编号从 0n - 1 。给你整数 n 和一个长度为 n - 1 的二维整数数组 edges ,其中 edges[i] = [ai, bi] 表示树中节点 aibi 之间有一条边。再给你一个长度为 n 的数组 coins ,其中 coins[i] 可能为 0 也可能为 11 表示节点 i 处有一个金币。

一开始,你需要选择树中任意一个节点出发。你可以执行下述操作任意次:

  • 收集距离当前节点距离为 2 以内的所有金币,或者
  • 移动到树中一个相邻节点。

你需要收集树中所有的金币,并且回到出发节点,请你返回最少经过的边数。

如果你多次经过一条边,每一次经过都会给答案加一。

  • 思路:拓扑排序

    • 首先,我们可以去掉不包含金币的子树,因为访问其中任何一个点都毫无意义。
    • 如果所有在叶子上的金币全部都能收集到,那么我们可以收集到树上所有金币。因此可以去除两轮叶子,剩余的结点即为必须经过的结点【两轮拓扑排序】
    • 从距离叶子为2的节点处出发【局部最优】,收集树中所有的金币,并且回到出发节点时经过的边数最少【全局最优】。
  • 实现

    • 无向图,节点度为1时为叶子
    class Solution {
        public int collectTheCoins(int[] coins, int[][] edges) {
            int n = coins.length;
            List<Integer> g[] = new ArrayList[n];
            Arrays.setAll(g, e -> new ArrayList<>());
            var deg = new int[n];
            for (var e : edges) {
                int x = e[0], y = e[1];
                g[x].add(y);
                g[y].add(x); // 建图
                ++deg[x];
                ++deg[y];
            }
    
            // 用拓扑排序「剪枝」:去掉没有金币的子树
            var q = new ArrayDeque<Integer>();
            for (int i = 0; i < n; ++i)
                if (deg[i] == 1 && coins[i] == 0) // 无金币叶子
                    q.add(i);
            while (!q.isEmpty()) {
                int x = q.peek();
                q.pop();
                for (int y : g[x])
                    if (--deg[y] == 1 && coins[y] == 0)
                        q.add(y);
            }
    
            // 再次拓扑排序
            for (int i = 0; i < n; ++i)
                if (deg[i] == 1 && coins[i] == 1) // 有金币叶子
                    q.add(i);
            if (q.size() <= 1) return 0; // 至多一个有金币的叶子,直接收集
            var time = new int[n];
            while (!q.isEmpty()) {
                int x = q.peek();
                q.pop();
                for (int y : g[x])
                    if (--deg[y] == 1) {
                        time[y] = time[x] + 1; // 记录入队时间
                        q.add(y);
                    }
            }
    
            // 统计答案
            int ans = 0;
            for (var e : edges)
                if (time[e[0]] >= 2 && time[e[1]] >= 2)
                    ans += 2;
            return ans;
        }
    }
    
    作者:灵茶山艾府
    链接:https://leetcode.cn/problems/collect-coins-in-a-tree/solutions/2191371/tuo-bu-pai-xu-ji-lu-ru-dui-shi-jian-pyth-6uli/
    来源:力扣(LeetCode)
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    
    • 复杂度

      • 时间复杂度: O ( E + V ) O(E+V) O(E+V) E E E表示邻边的跳数,V为结点的个数

      • 空间复杂度: O ( V ) O(V) O(V)

  • 升级:如果把题目中的 2 换成 0,1,2,3,⋯ ,n−1,你能把这些情况对应的答案全部算出来吗?

    遍历所有的边,如果两个节点的入队时间均大于q(最大距离),那么表示这条边必须要经过,结果+2

你可能感兴趣的:(周赛,算法,leetcode,贪心算法)