LeetCode周赛335,最后两题出反了吧?

72 阅读4分钟

大家好,我是梁唐。

今天是周一,我们照惯例来一起做一下昨天的LeetCode周赛。

不知不觉LeetCode周赛题解这个系列我已经写了一年多了,不出意外的话,以后还会继续更新下去。真心推荐有时间的同学都可以练习练习,保持手感这样就不必在面试之前特地花时间刷题了。

这一次是LeetCode周赛第335场,由LeetCode官方自己赞助自己举办。前10名以及一些特殊名次都有奖品。

这一套题做完之后,我个人的最大感受是最后两题的次序反了。写着Medium的第三题才是Hard难度,而第四题更像是那道Medium。

赛后看了一眼评论区,发现大家的观点也和我类似。

递枕头

n 个人站成一排,按从 1n 编号。

最初,排在队首的第一个人拿着一个枕头。每秒钟,拿着枕头的人会将枕头传递给队伍中的下一个人。一旦枕头到达队首或队尾,传递方向就会改变,队伍会继续沿相反方向传递枕头。

  • 例如,当枕头到达第 n 个人时,TA 会将枕头传递给第 n - 1 个人,然后传递给第 n - 2 个人,依此类推。

给你两个正整数 ntime ,返回 time 秒后拿着枕头的人的编号。

题解

签到题,枕头从1传到n,再从n传到1为一次循环。每次循环需要传递2n22n-2次。

我们想要知道time时间之后枕头的位置,只需要用它对2n22n-2取余即可。

class Solution {
public:
    int passThePillow(int n, int time) {
        int m = time % (2 * n - 2);

        if (m > n-1) return n - (m - n + 1);
            else return 1 + m;
    }
};

二叉树中的第 K 大层和

给你一棵二叉树的根节点 root 和一个正整数 k

树中的 层和 是指 同一层 上节点值的总和。

返回树中第 k 大的层和(不一定不同)。如果树少于 k 层,则返回 -1

注意,如果两个节点与根节点的距离相同,则认为它们在同一层。

题解

考察的是对二叉树的熟悉程度,我们可以使用递归遍历整棵树并求出每一层节点的总和。最后再对这些总和进行排序,基本上没有难度,主要是要对C++的STL比较熟悉。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    void dfs(TreeNode* u, int l, map<int, long long>&mp) {
        mp[l] += u->val;
        if (u->left != nullptr) {
            dfs(u->left, l+1, mp);
        }
        if (u->right != nullptr) {
            dfs(u->right, l+1, mp);
        }
    }
    long long kthLargestLevelSum(TreeNode* root, int k) {
        map<int, long long> mp;
        dfs(root, 1, mp);
        if (mp.size() < k) return -1;
        vector<long long> vt;
        for (auto it: mp) {
            vt.push_back(it.second);
        }
        sort(vt.begin(), vt.end(), greater<>());
        return vt[k-1];
    }
};

分割数组使乘积互质

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

如果在下标 i分割 数组,其中 0 <= i <= n - 2 ,使前 i + 1 个元素的乘积和剩余元素的乘积互质,则认为该分割 有效

  • 例如,如果 nums = [2, 3, 3] ,那么在下标 i = 0 处的分割有效,因为 29 互质,而在下标 i = 1 处的分割无效,因为 63 不互质。在下标 i = 2 处的分割也无效,因为 i == n - 1

返回可以有效分割数组的最小下标 i ,如果不存在有效分割,则返回 -1

当且仅当 gcd(val1, val2) == 1 成立时,val1val2 这两个值才是互质的,其中 gcd(val1, val2) 表示 val1val2 的最大公约数。

题解

这题看着就比较棘手,我们要寻找一个分段点使得数组前后两个部分的乘积互质。

然而C++当中能够表示的整形范围是有限的,最多只能表示大约410184 * 10^{18}的范围。在本题当中最多有一万个10610^6的数相乘,显然就超过范围了。并且我试了一下,虽然Python能够表示的整形没有范围限制,但本题的数据一样超过了Python能处理的范围。

所以我们必须要通过算法设计来解决这个问题,我们可以从互质的定义入手。如果两个数互质,说明它们最大公约数为1,也就是说它们没有其他共同的质因数。我们可以从这个角度入手,分别维护数组左右两个部分的质因数。只要这两个部分的质因数集合没有交集,那么就说明它们互质。但是直接遍历所有的质因数判断是否有交集需要遍历所有出现过的质因数,这会导致复杂度过大。

我们可以从质因数的角度入手,如果左右两个部分的乘积互质,那么对于某个特定的质因数来说,它只能全部出现在左侧或者右侧。也就是说如果我们判断左侧所有出现过的质因数都已经到齐了,没有遗漏在右侧的,那么也可以说明左右两边的乘积互质了。

我们使用map来记录集合内所有元素乘积的质因数,要做到这点,需要我们对每个元素分解质因数,然后再把各个元素分解之后的质因数合并,要分解质因数又需要我们先获取范围内的质数。获取一定范围内的所有质数可以使用筛法,这里有一个小技巧,本题的范围是10610^6这个范围内的质数数量有好几万,也是一个很大的数字。

但实际上我们只需要求出10310^3这个范围内的质数即可,因为如果一个10610^6范围内的数不能被10310^3范围内所有质数整除,那么说明它自身就是质数。所以我们只需要筛到10310^3这个范围即可,这样的话质数的数量要少得多,可以降低我们分解质因数的时候的复杂度。

本题的代码量比较大,逻辑也比较复杂,更多细节参考代码:

class Solution {
public:
    
    unordered_map<int, int> to_prime(int x, set<int>& prime) {
        // 分解质因数
        unordered_map<int, int> ret;
        for (auto p : prime) {
            // 说明已经分解完了
            if (x == 1) break;
            if (prime.count(x)) {
                ret[x]++;
                break;
            }
            // 能被质数p整除,就一直除
            while (x % p == 0) {
                ret[p]++;
                x /= p;
            }
        }
        if (x > 1) ret[x]++;
        return ret;
    }
    
    void merge(unordered_map<int, int>& mpa, unordered_map<int, int>& mpb) {
        // 合并两个map
        for (auto it: mpb) {
            mpa[it.first] += it.second;
        }
    }
    
    int findValidSplit(vector<int>& nums) {
        
        int maxi = 0;
        for (auto x: nums) maxi = max(maxi, x);
        maxi = sqrt(maxi) + 5;
        
        // 筛法求质数
        int is_prime[maxi+5];
        memset(is_prime, 0, sizeof is_prime);
        set<int> prime;
        
        for (int i = 2; i < maxi; i++) {
            if (is_prime[i]) continue;
            prime.insert(i);
            for (int j = i + i; j < maxi; j+=i) {
                is_prime[j] = 1;
            }
        }
        
        unordered_map<int, int> lef, tot, rig;
        
        // 求出每个质因数出现的次数
        for (auto x: nums) {
            unordered_map<int, int> cur = to_prime(x, prime);
            merge(tot, cur);
        }
        
        int cnt = 0;
        
        for (int i = 0; i < nums.size()-1; i++) {
            unordered_map<int, int> cur = to_prime(nums[i], prime);
            for (auto it: cur) {
                int k = it.first, v = it.second;
                lef[k] += v;
                // 如果k已经到齐,cnt+1
                if (lef[k] == tot[k]) cnt++;
            }
            // 如果每个质因数都没有遗漏
            if (cnt == lef.size()) return i;
        }
        return -1;
    }
};

获得分数的方法数

考试中有 n 种类型的题目。给你一个整数 target 和一个下标从 0 开始的二维整数数组 types ,其中 types[i] = [counti, marksi] 表示第 i 种类型的题目有 counti 道,每道题目对应 marksi 分。

返回你在考试中恰好得到 target 分的方法数。由于答案可能很大,结果需要对 109 +7 取余。

注意,同类型题目无法区分。

  • 比如说,如果有 3 道同类型题目,那么解答第 1 和第 2 道题目与解答第 1 和第 3 道题目或者第 2 和第 3 道题目是相同的。

题解

经典的多重背包问题,由于数据量比较小,我们直接枚举每种题目选取的数量即可。

背包容量是分数,状态是当前背包剩余的空间,策略是可以选取的题目。整个状态转移过程非常直观,基本上就是多重背包的裸题。

class Solution {
public:
    int waysToReachTarget(int target, vector<vector<int>>& types) {
        int n = types.size();
        
        int dp[target+10];
        memset(dp, 0, sizeof dp);
        
        int Mod = 1e9 + 7;
        dp[0] = 1;
        
        for (int i = 0; i < n; i++) {
            int c = types[i][0], v = types[i][1];
            for (int j = target; j >= 0; j--) {
                for (int k = 1; k <= c; k++) {
                    if (j < k * v) continue;
                    dp[j] = (dp[j] + dp[j - k * v]) % Mod;
                }
            }
        }
        return dp[target];
    }
};

确实从难度上来说,第四题要比第三题更容易一些。尤其是对于那些手熟的大佬来说,第四题基本上就是秒切的。