【LeetCode 周赛题解】第338场周赛题解

题目列表
6354. K 件物品的最大和(easy)
6355. 质数减法运算(medium)
6357. 使数组元素全部相等的最少操作次数(medium)
6356. 收集树中金币(hard)


6354. K 件物品的最大和

直接模拟,优先级为: + 1 > + 0 > − 1 +1>+0>-1 +1>+0>1

class Solution {
    
    
public:
    int kItemsWithMaximumSum(int numOnes, int numZeros, int numNegOnes, int k) {
    
    
        if(k<=numOnes)return k;
        else if(k<=numOnes+numZeros){
    
    //数量不够需要0凑数
            return numOnes;
        }
        else{
    
    
            k-=numOnes+numZeros;//还不够,不得不扣
            return numOnes-k;
        }
    }
};

6355. 质数减法运算

  从高到低,如果遇到 n u m s [ i ] > = n u m s [ i + 1 ] nums[i]>=nums[i+1] nums[i]>=nums[i+1] ,则寻找一个最小的质数 q q q ,使得 n u m s [ i ] − q < n u m s [ i + 1 ] nums[i]-q<nums[i+1] nums[i]q<nums[i+1] 。如果找到了, n u m s [ i ] = n u m s [ i ] − q nums[i]=nums[i]-q nums[i]=nums[i]q ,继续下一个数;如果找不到,返回 f a l s e false false
  以为1是质数,WA了一次。小学数学老师气死。

class Solution {
    
    
public:
    bool isPrime(int n){
    
    
        if(n==1) return false;
        if(n==2)return true;
        for(int i=2;i<=sqrt(n);i++){
    
    
            if(n%i==0)return false;
        }
        return true;
    }
    bool primeSubOperation(vector<int>& nums) {
    
    
        for(int i=nums.size()-2;i>=0;i--){
    
    
            if(nums[i]>=nums[i+1]){
    
    
                bool flag=false;
                for(int j=1;j<nums[i];j++){
    
    
                    if(isPrime(j)){
    
    
                        if(nums[i]-j<nums[i+1]){
    
    
                            nums[i]-=j;
                            flag=true;
                            break;
                        }
                    }
                }
                if(!flag) return flag;
            }
        }
        for(auto num:nums) cout<<num<<" ";
        cout<<endl;
        return true;
    }
};

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

  数据范围是 1 0 5 10^5 105,暴力必超。但是不信邪,也找不到更好的办法了,头铁了一下,贡献了一个WA。 O ( n ) O(n) O(n)时间也是不可能的,所以这题的优化肯定在 O ( n l o g n ) O(nlogn) O(nlogn) 上。提到 O ( n l o g n ) O(nlogn) O(nlogn),那必然就涉及排序、二分等等。这题也确实是这个方向。

  观察可以发现,每次大于 q u e r i e s [ i ] queries[i] queries[i] 的数字得往下减,小于的得往上加。那能不能不一个一个减,一次就把小于 q u e r i e s [ i ] queries[i] queries[i] 的数拉到等于 q u e r i e s [ i ] queries[i] queries[i] ,再一次性把大于 q u e r i e s [ i ] queries[i] queries[i] 的数拉到等于 q u e r i e s [ i ] queries[i] queries[i] 。当然可以,假设小于 q u e r i e s [ i ] queries[i] queries[i] 的数一共有 n n n 个,且这 n n n 个数的和为 s u m sum sum ,则 + 1 +1 +1 的操作次数为 q u e r i e s [ i ] ∗ n − s u m queries[i]*n-sum queries[i]nsum ,大于的部分同理。如果这样做的话,我们每次需要分别统计大于和小于 q u e r i e s [ i ] queries[i] queries[i] 的数的个数,还得分别算他们的和,貌似并没有优化。但是如果我们把原数组排序,并且计算一个前缀和,那我们可以用二分查找,查找 q u e r i e s [ i ] queries[i] queries[i] 在排序后的数组里面的位置,就可以一次性得到上述所需的四个值。

class Solution {
    
    
public:
    vector<long long> minOperations(vector<int>& nums, vector<int>& queries) {
    
    
        int m=queries.size();
        int n=nums.size();
        vector<long long> res(m,0);
        vector<long long> sum(n+1,0);
        sort(nums.begin(),nums.end());
        for(int i=1;i<=n;i++){
    
    
            sum[i]+=sum[i-1]+nums[i-1];
        }
        for(int i=0;i<m;i++){
    
    
            long long q=queries[i];
            int idx=upper_bound(nums.begin(),nums.end(),q)-nums.begin();
            // cout<<idx<<endl;
            res[i]+=sum[n]-sum[idx]-q*(n-idx)+(idx)*q-sum[idx];
        }
        return res;
    }
};

6356. 收集树中金币

  这种题再给我十个脑子我也想不出来,像极了数学证明题。看着答案都要想半天才知道为什么要这么做。

  先把没金币的子树删掉,因为没必要走过去。此时剩下的拓扑就是都得去走一遍的;然后我们从叶子节点往上走一次,并记录每个点进队的时间。由于是从叶子倒着走的,所以其实这个时间就代表了该点到叶子节点的距离。神奇之处在于,这么走一遍之后,所有两端节点的时间大于2的边,都是需要经过的边,且由于需要回到起点,经过次数为两次。换个说法就是,没有任何一条边需要走三次,且每一条边都要走两次!我也很Amazing。因为主干道上的金币在走的过程中顺路收集,而叶子节点不用走到头,所以其实考虑走到何处可以拿到叶子节点上的金币,并在此处停下掉头就行了。至于为什么不会有经过超过两次的边,其实知道这个结论之后去举例子,发现确实很容易证明,难点在于怎么想得到这个约束条件呢。

class Solution {
    
    
public:
    int collectTheCoins(vector<int>& coins, vector<vector<int>>& edges) {
    
    
        int n=coins.size();
        vector<vector<int>> g(n);//邻接表
        vector<int> deg(n,0);//每个节点的度
        for(auto e:edges){
    
    
            int x=e[0],y=e[1];
            g[x].push_back(y);
            g[y].push_back(x);
            deg[x]++;
            deg[y]++;
        }
        //删掉无金币的子树
        queue<int> q;
        for(int i=0;i<n;i++){
    
    
            if(deg[i]==1&&coins[i]==0){
    
    //度为一,即叶子节点,且无金币
                q.push(i);
            }
        }
        while(!q.empty()){
    
    
            int x=q.front();
            q.pop();
            for(auto y:g[x]){
    
    
                //由于队里都是无金币叶子,故y就是与其相连的唯一的一个节点,也就是其父亲
                //如果删掉当前叶子之后父亲的度为1且父亲也没金币,那父亲也变成了无金币叶子,应该在下一轮被删掉,故入队
                //而且由于第二次访问的时候也是倒着走的,且留下的都是有金币的叶子,我们直接度减一就可以做到删掉该节点,而无需去删对应的边
                if(--deg[y]==1&&coins[y]==0){
    
    
                    q.push(y);
                }
            }
        }
        //现在只剩下有金币叶子及其路径了,再走一遍,计算每个节点到叶子的距离
        for(int i=0;i<n;i++){
    
    
            if(deg[i]==1&&coins[i]){
    
    
                q.push(i);
            }
        }
        if(q.size()<=1) return 0;//如果只有一个叶子有金币,或完全无金币,则不需要拿金币或站在起点就能拿到金币
        vector<int> time(n,0);
        while(!q.empty()){
    
    
            int x=q.front();
            q.pop();
            for(auto y:g[x]){
    
    
                if(--deg[y]==1){
    
    //只有当孩子全都没了,当前节点是叶子了,才入队并更新时间
                    time[y]=time[x]+1;
                    q.push(y);
                }
            }
        }
        int res=0;
        for(auto e:edges){
    
    
            int x=e[0],y=e[1];
            if(time[x]>=2&&time[y]>=2) res+=2;
        }
        return res;
    }
};

猜你喜欢

转载自blog.csdn.net/qq_44623371/article/details/129777626