LeetCode周赛320,时隔一月的内推机会

147 阅读5分钟

大家好,我是梁唐。

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

这一场是LeetCode周赛的第320场,由诺基亚贝尔赞助。前200名的同学可以获得简历内推的机会。

这一场的比赛赛题质量还可以,但翻译上有一点缺陷,如果读的是中文的题面在理解上可能会存在一点小问题。。。

很多人吐槽了这个问题,我做题的时候也蒙了一会,错了一次之后才找到原因……

好了,废话不多说,我们来看题吧。

数组中不等三元组的数目

给你一个下标从 0 开始的正整数数组 nums 。请你找出并统计满足下述条件的三元组 (i, j, k) 的数目:

  • 0 <= i < j < k < nums.length

  • nums[i]、nums[j] 和 nums[k] 两两不同

    • 换句话说:nums[i] != nums[j]、nums[i] != nums[k] 且 nums[j] != nums[k] 。

返回满足上述条件三元组的数目*。*

题解

水题,范围太小,直接O(n3)O(n^3)暴力循环搞定。

class Solution {
public:
    int unequalTriplets(vector<int>& nums) {
        int ret = 0;
        int n = nums.size();
        for (int i = 0; i < n; i++) {
            for (int j = i+1; j < n; j++) {
                for (int k = j+1; k < n; k++) {
                    if (nums[i] != nums[j] && nums[i] != nums[k] && nums[j] != nums[k]) ret++;
                }
            }
        }
        return ret;
    }
};

二叉搜索树最近节点查询

给你一个 二叉搜索树 的根节点 root ,和一个由正整数组成、长度为 n 的数组 queries 。

请你找出一个长度为 n 的 二维 答案数组 answer ,其中 answer[i] = [mini, maxi] :

  • mini 是树中小于等于 queries[i] 的 最大值 。如果不存在这样的值,则使用 -1 代替。
  • maxi 是树中大于等于 queries[i] 的 最小值 。如果不存在这样的值,则使用 -1 代替。

返回数组 answer 。

题解

这题想到解法不难,无非是二分搜索找到答案。但扯的是,题目给定的虽然是二叉搜索树,但不保证平衡。也就是说会存在蜕化成链表的极端case。如果直接在二叉搜索树上搜索会超时,所以只能先通过中序遍历的方式拿到所有的元素,再通过二分来找答案。

但这么一来二叉搜索树的数据结构就显得多余了,即使我们拿到元素不是按照递增或递减的顺序,也只需要排序一次即可。

/**
 * 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(vector<int> &vt, TreeNode* u) {
        if (u == nullptr) return ;
        dfs(vt, u->left);
        vt.push_back(u->val);
        dfs(vt, u->right);
    }

    vector<vector<int>> closestNodes(TreeNode* root, vector<int>& queries) {
        vector<vector<int>> ret;

        vector<int> vt;
        dfs(vt, root);
        for (auto q : queries) {
            // lower_bound找到第一个大于等于q的位置
            auto it = lower_bound(vt.begin(), vt.end(), q);
            if (it == vt.end()) {
                ret.push_back({vt.back(), -1});
            }else if (*it == q) {
                ret.push_back({q, q});
            }else if (it == vt.begin()) {
                ret.push_back({-1, *it});
            }else {
                ret.push_back({*prev(it), *it});
            }
        }
        return ret;
    }
};

到达首都的最少油耗

给你一棵 n 个节点的树(一个无向、连通、无环图),每个节点表示一个城市,编号从 0 到 n - 1 ,且恰好有 n - 1 条路。0 是首都。给你一个二维整数数组 roads ,其中 roads[i] = [ai, bi] ,表示城市 ai 和 bi 之间有一条 双向路

每个城市里有一个代表,他们都要去首都参加一个会议。

每座城市里有一辆车。给你一个整数 seats 表示每辆车里面座位的数目。

城市里的代表可以选择乘坐所在城市的车,或者乘坐其他城市的车。相邻城市之间一辆车的油耗是一升汽油。

请你返回到达首都最少需要多少升汽油。

题解

首先题目给定我们树是以边的形式,我们最好将它使用邻接表或者其他数据结构进行转化,这样方便我们进行递归。

其次,经过简单分析,可以发现距离根节点也就是节点0最远的节点,也就是叶子节点,这些节点只有一种解法,就是乘坐本节点的车去往更上层的节点。我们继续分析,对于叶子节点上一层的节点而言,它们的代表就有两种方式,一种是搭便车,另外一种是单独再开一辆车继续往上。

我们要使得总体耗油量越少,显然开出的车数量越少,消耗的油就越少。这里有一个坑点,题目中没有明确说可以在节点处换乘,比如有10个人乘坐10辆车来了当前节点,而一辆车就能装下10个人,那么就合乘一辆车出发,这样可以省油。

这里我使用了一个pair来记录当前节点开出往根节点进发的车的数量和乘客的数量,之后只需要递归即可。

class Solution {
public:

    typedef pair<long long, long long> pll;

    pll dfs(vector<vector<int>>& graph, int u, int f, int seats, long long &ret) {
        int cars = 0, persons = 1;
        for (auto v: graph[u]) {
            if (v == f) continue;
            auto cur = dfs(graph, v, u, seats, ret);
            
            cars += cur.first;
            persons += cur.second;
            // 从子节点开来车的数量就是耗油的数量
            ret += cur.first;
        }

        // 换乘,尽可能少得开车
        cars = (persons + seats - 1) / seats;
        return pll(cars, persons);
    }

    long long minimumFuelCost(vector<vector<int>>& roads, int seats) {
        int n = roads.size();

        // 邻接表建树
        vector<vector<int>> graph(n+1, vector<int>());

        for (auto &vt: roads) {
            int u = vt[0], v = vt[1];
            graph[u].push_back(v);
            graph[v].push_back(u);
        }

        long long ret = 0;
        dfs(graph, 0, -1, seats, ret);
        return ret;
    }
};

完美分割的方案数

给你一个字符串 s ,每个字符是数字 '1' 到 '9' ,再给你两个整数 k 和 minLength 。

如果对 s 的分割满足以下条件,那么我们认为它是一个 完美 分割:

  • s 被分成 k 段互不相交的子字符串。
  • 每个子字符串长度都 至少 为 minLength 。
  • 每个子字符串的第一个字符都是一个 质数 数字,最后一个字符都是一个 非质数 数字。质数数字为 '2' ,'3' ,'5' 和 '7' ,剩下的都是非质数数字。

请你返回 s 的 完美 分割数目。由于答案可能很大,请返回答案对 109 + 7 取余 后的结果。

一个 子字符串 是字符串中一段连续字符串序列。

题解

很显然,这是一道动态规划问题。

我们使用dp[i][k]表示以下标i结尾(下标从1开始),将字符串分割成k段,满足题意的解法的数量。我们要求dp[i][k]就需要先找到一个更早的状态dp[j][k-1],并且满足字符串s[j+1:i]满足题意。这样dp[i][k] += dp[j][k-1]。我们遍历所有可能的j,就能求出答案。

理解了状态转移方程之后,整个代码非常简单,是这样的:

class Solution {
public:
    int beautifulPartitions(string s, int k, int minLength) {
        long long Mod = 1e9 + 7;
        int n = s.length();
        vector<vector<long long>> dp(n+2, vector<long long>(k+2, 0));
        dp[0][0] = 1;

        set<char> prime {'2', '3', '5', '7'};

        // 字符串向右移动一格,将下标0表示空串
        s = ' ' + s;
        if (!prime.count(s[1])) return 0;

        // 字符串长度需要大于等于minLength,所以i从minLength开始
        for (int i = minLength; i <= n; i++) {
            // s[i]不能是质数
            if (prime.count(s[i])) continue;
            // 遍历所有可能的j,保证s[j:i]长度大于等于minLength
            for (int j = 1; j <= i-minLength+1; j++) {
                // s[j] 不能是合数
                if (!prime.count(s[j])) continue;
                for (int _k = 1; _k <= k; _k++) {
                    dp[i][_k] = (dp[i][_k] + dp[j-1][_k-1]) % Mod;
                }
            }
        }

        return dp[n][k];
    }
};

但这样并不能通过,因为复杂度高达O(n2k)O(n^2k),会超时。所以我们需要对这个计算过程进行优化,怎么优化呢?

观察一下代码,可以发现我们遍历j的时候,它的范围和i的范围是挂钩的。我们可以使用前缀和来进行优化,用pres[i][k]来记录j从1遍历到i对应的dp[j][k]的和。因为j的遍历范围和i是对应的,我们只需要在每一轮循环结束之后累加更新pres即可。

可以比较上下两段代码,感受一下前缀和的妙用。

class Solution {
public:
    int beautifulPartitions(string s, int k, int minLength) {
        long long Mod = 1e9 + 7;
        int n = s.length();
        vector<vector<long long>> dp(n+2, vector<long long>(k+2, 0)), pres(n+2, vector<long long>(k+2, 0));
        dp[0][0] = 1;
        pres[0][0] = 1;

        set<char> prime {'2', '3', '5', '7'};

        s = ' ' + s;

        if (!prime.count(s[1])) return 0;

        for (int i = 1; i <= n; i++) {
        	// s[i]要是合数,且s[i+1]必须是质数
            if (i >= minLength && !prime.count(s[i]) && (i == n || prime.count(s[i+1]))) {
                // 前缀和即是答案
                for (int _k = 1; _k <= k; _k++) {
                    dp[i][_k] = pres[i-minLength][_k-1];
                }
            }

            // 更新前缀和准备下一轮遍历
            for (int _k = 0; _k <= k; _k++) {
                pres[i][_k] = (pres[i-1][_k] + dp[i][_k]) % Mod;
            }
        }

        return dp[n][k];
    }
};

我个人感觉这一场的题目质量还是不错的,尤其是最后两题,涉及的解法和思路之前很少出现。非常适合大家练习。