周赛338(模拟、贪心+质数、排序+前缀和+二分查找、拓扑排序)

文章目录

  • 周赛338
    • [6354. K 件物品的最大和](https://leetcode.cn/problems/k-items-with-the-maximum-sum/)
      • 模拟 分类讨论
    • [6355. 质数减法运算](https://leetcode.cn/problems/prime-subtraction-operation/)
      • 贪心 + 欧拉筛
    • [6357. 使数组元素全部相等的最少操作次数](https://leetcode.cn/problems/minimum-operations-to-make-all-array-elements-equal/)
      • 排序 + 前缀和 + 二分查找
    • [6356. 收集树中金币](https://leetcode.cn/problems/collect-coins-in-a-tree/)
      • 拓扑排序(层层剥离)

周赛338

6354. K 件物品的最大和

难度简单0

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

给你四个非负整数 numOnesnumZerosnumNegOnesk

袋子最初包含:

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

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

示例 1:

输入:numOnes = 3, numZeros = 2, numNegOnes = 0, k = 2
输出:2
解释:袋子中的物品分别标记为 {1, 1, 1, 0, 0} 。取 2 件标记为 1 的物品,得到的数字之和为 2 。
可以证明 2 是所有可行方案中的最大值。

示例 2:

输入:numOnes = 3, numZeros = 2, numNegOnes = 0, k = 4
输出:3
解释:袋子中的物品分别标记为 {1, 1, 1, 0, 0} 。取 3 件标记为 1 的物品,1 件标记为 0 的物品,得到的数字之和为 3 。
可以证明 3 是所有可行方案中的最大值。

提示:

  • 0 <= numOnes, numZeros, numNegOnes <= 50
  • 0 <= k <= numOnes + numZeros + numNegOnes

模拟 分类讨论

class Solution {
    public int kItemsWithMaximumSum(int numOnes, int numZeros, int numNegOnes, int k) {
        if(k <= numOnes) return k;
        else if(k <= numOnes + numZeros) return numOnes;
        else return numOnes - (k - numOnes - numZeros);// 7-5-1
    }
}

6355. 质数减法运算

难度中等2

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

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

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

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

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

示例 1:

输入:nums = [4,9,6,10]
输出:true
解释:
在第一次运算中:选择 i = 0 和 p = 3 ,然后从 nums[0] 减去 3 ,nums 变为 [1,9,6,10] 。
在第二次运算中:选择 i = 1 和 p = 7 ,然后从 nums[1] 减去 7 ,nums 变为 [1,2,6,10] 。
第二次运算后,nums 按严格递增顺序排序,因此答案为 true 。

示例 2:

输入:nums = [6,8,11,12]
输出:true
解释:nums 从一开始就按严格递增顺序排序,因此不需要执行任何运算。

示例 3:

输入:nums = [5,8,3]
输出:false
解释:可以证明,执行运算无法使 nums 按严格递增顺序排序,因此答案是 false 。

提示:

  • 1 <= nums.length <= 1000
  • 1 <= nums[i] <= 1000
  • nums.length == n

贪心 + 欧拉筛

  • 贪心:让前一个数每次都最小
class Solution {
    public boolean primeSubOperation(int[] nums) {
        int n = 2000;
        // 欧拉筛
        //判断是否是质数,1-质数 0-合数
        int[] isPrime = new int[n];
        //存放质数
        int[] primes = new int[n];
        int k = 0;//存放质数数组的索引下标
        Arrays.fill(isPrime, 1);
        isPrime[1] = 0;
        for (int i = 2; i < n; i++) {
            if (isPrime[i] == 1) {
               primes[k++] = i;
            }
            //枚举已经筛出来的素数prime[j](j=1~cnt)
            for (int j = 0; primes[j] * i < n; j++) {
                //筛掉i的素数倍,即i的prime[j]倍
                isPrime[primes[j] * i] = 0;//每个质数都和i相乘得到合数
                //如果i整除prime[j],退出循环,保证线性时间复杂度
                if (i % primes[j] == 0) {//primes[j]是i的一个质因数
                    break;
                }
            }
        }
        int pre = 0; //  当前枚举i位上前一个元素值(每次都让其最小)
        for(int i = 0; i < nums.length; i++){
            int cur = nums[i];
            // 不满足直接false,因为pre已经是最小了,cur还比pre小
            if(cur <= pre) return false;
            // 让当前数达到能达到的最小值
            while(true){
                if(isPrime[cur] == 1){
                    if(pre < nums[i] - cur){
                        pre = nums[i] - cur;
                        break;
                    }
                }
                cur--;
            }
        }
        return true;
    }

6357. 使数组元素全部相等的最少操作次数

难度中等0

给你一个正整数数组 nums

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

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

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

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

示例 1:

输入:nums = [3,1,6,8], queries = [1,5]
输出:[14,10]
解释:第一个查询,我们可以执行以下操作:
- 将 nums[0] 减小 2 次,nums = [1,1,6,8] 。
- 将 nums[2] 减小 5 次,nums = [1,1,1,8] 。
- 将 nums[3] 减小 7 次,nums = [1,1,1,1] 。
第一个查询的总操作次数为 2 + 5 + 7 = 14 。
第二个查询,我们可以执行以下操作:
- 将 nums[0] 增大 2 次,nums = [5,1,6,8] 。
- 将 nums[1] 增大 4 次,nums = [5,5,6,8] 。
- 将 nums[2] 减小 1 次,nums = [5,5,5,8] 。
- 将 nums[3] 减小 3 次,nums = [5,5,5,5] 。
第二个查询的总操作次数为 2 + 4 + 1 + 3 = 10 。

示例 2:

输入:nums = [2,9,6,3], queries = [10]
输出:[20]
解释:我们可以将数组中所有元素都增大到 10 ,总操作次数为 8 + 1 + 4 + 7 = 20 。

提示:

  • n == nums.length
  • m == queries.length
  • 1 <= n, m <= 105
  • 1 <= nums[i], queries[i] <= 109

排序 + 前缀和 + 二分查找

思考过程:https://leetcode.cn/problems/minimum-operations-to-make-all-array-elements-equal/solution/pai-xu-qian-zhui-he-er-fen-cha-zhao-by-s-t2mn/

每次+1或-1,那么其实就是每个数对当前queries[i]的差值,一个一个算肯定不行,举个例子对于3来说,1和2于它的差值为2*3-(1+2),那么其实就是n个3相乘减去n个数的和,那么我们可以利用前缀和加速

数组中有大于等于queries[i]和小于的,那么分成两段计算,我们就要找一个分界点,我们对数组排序,通过二分查找找到第一个>=queries[i]的位置,然后对这个位置分两段来计算

特殊情况,所有数都小于queries[i],那么我们就不用分类了,直接算就行

  • 目标和 - 前缀和的思想(重要!!!)
class Solution {
    public List<Long> minOperations(int[] nums, int[] queries) {
        Arrays.sort(nums);
        List<Long> res = new ArrayList<>();
        int n = nums.length;
        long[] sum = new long[n+1];
        for(int i = 0; i < n; i++){
            sum[i+1] = sum[i] + nums[i];
        }
        for(int q : queries){
            // 找到第一个比q大的元素下标
            int left = 0, right = n;
            while(left < right) {
                int mid = (left + right) >> 1;
                if(nums[mid] <= q) left = mid + 1;
                else right = mid;
            }
            // 在left=right左边的都比q小,右边的都比q大
            // 用目标和-前缀和求左边的操作数 用前缀和-目标和求右边的操作数
            long cur = 0l;
            cur += (long)q * (long)left - sum[left]; //左边: 目标和 - 前缀和
            cur += sum[n] - sum[left] - (long)q * (long)(n-left); //右边:前缀和 - 目标和
            res.add((long)cur);
        }
        return res;
    }
}

Go

func minOperations(nums []int, queries []int) []int64 {
    sort.Ints(nums);
    n := len(nums)
    s := make([]int, n+1)
    for i := 0; i < n; i++ {
        s[i+1] = s[i] + nums[i]
    }
    res := make([]int64, len(queries))
    for i, q := range queries {
        // 在nums数组中找到第一个小于q的元素位置
        // go有api可以方便查找
        //right := sort.SearchInts(nums, q)
        left, right := 0, n
        for left < right {
            mid := (left + right) >> 1;
            if nums[mid] < q {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        l := right * q - s[right];
        r := s[n] - s[right] - (n - right) * q
        res[i] += int64(l + r)
    }
    return res
}

6356. 收集树中金币

难度困难5

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

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

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

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

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

示例 1:

周赛338(模拟、贪心+质数、排序+前缀和+二分查找、拓扑排序)_第1张图片

输入:coins = [1,0,0,0,0,1], edges = [[0,1],[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:从节点 2 出发,收集节点 0 处的金币,移动到节点 3 ,收集节点 5 处的金币,然后移动回节点 2 。

示例 2:

周赛338(模拟、贪心+质数、排序+前缀和+二分查找、拓扑排序)_第2张图片

输入:coins = [0,0,0,1,1,0,0,1], edges = [[0,1],[0,2],[1,3],[1,4],[2,5],[5,6],[5,7]]
输出:2
解释:从节点 0 出发,收集节点 4 和 3 处的金币,移动到节点 2 处,收集节点 7 处的金币,移动回节点 0 。

提示:

  • n == coins.length
  • 1 <= n <= 3 * 104
  • 0 <= coins[i] <= 1
  • edges.length == n - 1
  • edges[i].length == 2
  • 0 <= ai, bi < n
  • ai != bi
  • edges 表示一棵合法的树。

拓扑排序(层层剥离)

题解:https://leetcode.cn/problems/collect-coins-in-a-tree/solution/ceng-ceng-bo-chi-mei-yong-de-jie-dian-le-w10u/

0x3f:https://leetcode.cn/problems/collect-coins-in-a-tree/solution/tuo-bu-pai-xu-ji-lu-ru-dui-shi-jian-pyth-6uli/

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<>());
        int[] indegree = new int[n]; // 入度
        for(int[] e : edges){
            int x = e[0], y = e[1];
            g[x].add(y);
            g[y].add(x);//建图
            ++indegree[x];
            ++indegree[y];//
        }

        // 用拓扑排序剪枝:去掉没有金币的子树
        Deque<Integer> dq = new ArrayDeque<>();
        for(int i = 0; i < n; i++){
            if(indegree[i] == 1 && coins[i] == 0){ // 无金币叶子
                dq.add(i);
            } 
        }
        while(!dq.isEmpty()){
            int x = dq.peek();
            dq.pop();
            for(int y : g[x]){
                if(--indegree[y] == 1 && coins[y] == 0) { // 无金币叶子的链路都不要
                    dq.add(y);
                }
            }
        }
        // 再跑两次拓扑排序:删除树中所有叶子节点,及其相邻边(删两次)

        // 所有的含金币的叶子节点,其实也是不需要被访问的,只需要距离 < 2 就可以了
        for(int i = 0; i < n; i++){
            if(indegree[i] == 1 && coins[i] == 1){ // 有金币的叶子
                dq.add(i);
            } 
        }
        if(dq.size() <= 1) return 0; // 至多一个有金币的叶子,直接收集
		
        // 我们得到了一个 ”核心树“,其中每个节点都必须要访问。
        // 从树的任意节点出发,访问所有节点然后返回,每条边恰好访问两次。因此答案就是核心树上的边数
        int[] time = new int[n];
        while(!dq.isEmpty()){
            int x = dq.peek();
            dq.pop();
            for(int y : g[x]){
                if(--indegree[y] == 1) { // 记录入队时间
                    time[y] = time[x] + 1;
                    dq.add(y);
                }
            }
        }

        // 统计答案 答案就是剩下的树中的边数
        int ans = 0;
        for(int[] e : edges){
            if(time[e[0]] >= 2 && time[e[1]] >= 2){
                ans += 2;
            }
        }
        return ans;
    }
}

你可能感兴趣的:(算法刷题记录,leetcode)