Bootstrap

Leetcode 第 420 场周赛题解

Leetcode 第 420 场周赛题解

题目1:3324. 出现在屏幕上的字符串序列

思路

模拟整个过程。

初始化字符串 s 为空。

对于每一个 target[i],先在字符串 s 的末尾插入一个 ‘a’,然后模拟 s[i] 从 ‘a’ 到 target[i] 的过程,每更新一次 s,就将 s 插入到答案数组中。

代码

/*
 * @lc app=leetcode.cn id=3324 lang=cpp
 *
 * [3324] 出现在屏幕上的字符串序列
 */

// @lc code=start
class Solution
{
public:
    vector<string> stringSequence(string target)
    {
        if (target.empty())
            return {};

        string s;
        vector<string> ans;
        for (int i = 0; i < target.length(); i++)
        {
            s.push_back('a');
            for (char c = 'a'; c <= target[i]; c++)
            {
                s.back() = c;
                ans.push_back(s);
            }
        }

        return ans;
    }
};
// @lc code=end

复杂度分析

时间复杂度:O(n2 * ∣Σ∣),其中 n 是字符串 target 的长度,∣Σ∣=26 是字符集合的大小。

空间复杂度:O(n),其中 n 是字符串 target 的长度。

题目2:3325. 字符至少出现 K 次的子字符串 I

思路

暴力会超时:

/*
 * @lc app=leetcode.cn id=3325 lang=cpp
 *
 * [3325] 字符至少出现 K 次的子字符串 I
 */

// @lc code=start
class Solution
{
public:
    int numberOfSubstrings(string s, int k)
    {
        int n = s.length();
        if (k > n)
            return 0;

        int count = 0;
        for (int i = 0; i < n; i++)
            for (int len = 1; i + len <= n; len++)
            {
                string temp = s.substr(i, len);
                if (check(temp, k))
                    count++;
            }

        return count;
    }
    // 辅函数
    bool check(string &s, int k)
    {
        unordered_map<int, int> hashMap;
        for (char &c : s)
            hashMap[c]++;

        for (auto &[c, cnt] : hashMap)
            if (cnt >= k)
                return true;

        return false;
    }
};
// @lc code=end

在这里插入图片描述

我们可以用滑动窗口求解。

从小到大枚举子串右端点 right,如果子串符合要求,则右移左端点 left。

滑动窗口的内层循环结束时,右端点固定在 right,左端点在 0、1、2、⋯、left−1 的所有子串都是合法的,这一共有 left 个,加入答案。

代码

/*
 * @lc app=leetcode.cn id=3325 lang=cpp
 *
 * [3325] 字符至少出现 K 次的子字符串 I
 */

// @lc code=start
class Solution
{
public:
    int numberOfSubstrings(string s, int k)
    {
        int n = s.length();
        if (k > n)
            return 0;

        vector<int> cnt(26, 0);
        int left = 0; // 滑动窗口的左端点
        int count = 0;
        for (int right = 0; right < n; right++)
        {
            cnt[s[right] - 'a']++;

            while (cnt[s[right] - 'a'] >= k)
            {
                cnt[s[left] - 'a']--;
                left++;
            }

            count += left;
        }

        return count;
    }
};
// @lc code=end

复杂度分析

时间复杂度:O(n+∣Σ∣),其中 n 是字符串 s 的长度,∣Σ∣=26 是字符集合的大小。

空间复杂度:O(∣Σ∣),∣Σ∣=26 是字符集合的大小。

题目3:3326. 使数组非递减的最少除法操作次数

思路

定义 LPF(x) 为 x 的最小质因子。规定 LPF(1)=1。

  • 如果 LPF(x)=x,说明 x 是 1 或者质数,无法变小。
  • 如果 LPF(x)<x,说明 x 是合数,可以变小。由于题目规定只能除以最大真因数,我们可以把 x 除以 x / LPF(x),得到 LPF(x)。

贪心,最后一个数肯定无需减少,所以我们从 i=n−2 开始倒着遍历 nums:

如果 nums[i]>nums[i+1],那么把 nums[i] 更新为 LPF(nums[i]),操作次数加一。注意更新后 nums[i] 一定是质数或 1,无法再变小。

更新后,如果 nums[i]>nums[i+1] 仍然成立,说明无法把 nums 变成非降的,返回 −1。

代码

/*
 * @lc app=leetcode.cn id=3326 lang=cpp
 *
 * [3326] 使数组非递减的最少除法操作次数
 */

// @lc code=start

// 预处理
const int MX = 1e6 + 1;
int LPF[MX];

auto init = []
{
    for (int i = 2; i < MX; i++)
        if (LPF[i] == 0)
            for (int j = i; j < MX; j += i)
                if (LPF[j] == 0)
                    LPF[j] = i;

    return 0;
}();

class Solution
{
public:
    int minOperations(vector<int> &nums)
    {
        int ans = 0;

        for (int i = nums.size() - 2; i >= 0; i--)
        {
            if (nums[i] > nums[i + 1])
            {
                nums[i] = LPF[nums[i]];
                if (nums[i] > nums[i + 1])
                    return -1;
                ans++;
            }
        }

        return ans;
    }
};
// @lc code=end

复杂度分析

预处理的时间复杂度为 O(UloglogU),其中 U=106

时间复杂度:O(n),其中 n 是数组 nums 的长度。

空间复杂度:O(1)。

题目4:3327. 判断 DFS 字符串是否是回文串

思路

题解:https://leetcode.cn/problems/check-if-dfs-strings-are-palindromes/solutions/2957704/mo-ban-dfs-shi-jian-chuo-manacher-suan-f-ttu6/

构造 dfsStr 的过程是后序遍历。

子树的后序遍历字符串,是整棵树的后序遍历字符串的子串。

后序遍历的同时,计算每个节点 i 在后序遍历中的开始时间戳和结束时间戳,这也是子树 i 的后序遍历字符串在 dfsStr 上的开始下标和结束下标(代码用的左闭右开区间)。

在 dfsStr 上跑 Manacher 算法,这样就可以 O(1) 判断任意子串是否回文了。

代码

/*
 * @lc app=leetcode.cn id=3327 lang=cpp
 *
 * [3327] 判断 DFS 字符串是否是回文串
 */

// @lc code=start
class Solution
{
public:
    vector<bool> findAnswer(vector<int> &parent, string s)
    {
        int n = parent.size();
        vector<vector<int>> g(n);
        for (int i = 1; i < n; i++)
        {
            int p = parent[i];
            // 由于 i 是递增的,所以 g[p] 必然是有序的,下面无需排序
            g[p].push_back(i);
        }

        // dfsStr 是后序遍历整棵树得到的字符串
        string dfsStr(n, 0);
        // nodes[i] 表示子树 i 的后序遍历的开始时间戳和结束时间戳+1(左闭右开区间)
        vector<pair<int, int>> nodes(n);
        int time = 0;
        auto dfs = [&](auto &&dfs, int x) -> void
        {
            nodes[x].first = time;
            for (int y : g[x])
                dfs(dfs, y);
            dfsStr[time++] = s[x]; // 后序遍历
            nodes[x].second = time;
        };
        dfs(dfs, 0);

        // Manacher 模板
        // 将 dfsStr 改造为 t,这样就不需要讨论 n 的奇偶性,因为新串 t 的每个回文子串都是奇回文串(都有回文中心)
        // dfsStr 和 t 的下标转换关系:
        // (dfsStr_i+1)*2 = ti
        // ti/2-1 = dfsStr_i
        // ti 为偶数,对应奇回文串(从 2 开始)
        // ti 为奇数,对应偶回文串(从 3 开始)
        string t = "^";
        for (char c : dfsStr)
        {
            t += '#';
            t += c;
        }
        t += "#$";

        // 定义一个奇回文串的回文半径=(长度+1)/2,即保留回文中心,去掉一侧后的剩余字符串的长度
        // halfLen[i] 表示在 t 上的以 t[i] 为回文中心的最长回文子串的回文半径
        // 即 [i-halfLen[i]+1,i+halfLen[i]-1] 是 t 上的一个回文子串
        vector<int> halfLen(t.length() - 2);
        halfLen[1] = 1;
        // boxR 表示当前右边界下标最大的回文子串的右边界下标+1
        // boxM 为该回文子串的中心位置,二者的关系为 r=mid+halfLen[mid]
        int boxM = 0, boxR = 0;
        for (int i = 2; i < halfLen.size(); i++)
        { // 循环的起止位置对应着原串的首尾字符
            int hl = 1;
            if (i < boxR)
            {
                // 记 i 关于 boxM 的对称位置 i'=boxM*2-i
                // 若以 i' 为中心的最长回文子串范围超出了以 boxM 为中心的回文串的范围(即 i+halfLen[i'] >= boxR)
                // 则 halfLen[i] 应先初始化为已知的回文半径 boxR-i,然后再继续暴力匹配
                // 否则 halfLen[i] 与 halfLen[i'] 相等
                hl = min(halfLen[boxM * 2 - i], boxR - i);
            }
            // 暴力扩展
            // 算法的复杂度取决于这部分执行的次数
            // 由于扩展之后 boxR 必然会更新(右移),且扩展的的次数就是 boxR 右移的次数
            // 因此算法的复杂度 = O(len(t)) = O(n)
            while (t[i - hl] == t[i + hl])
            {
                hl++;
                boxM = i;
                boxR = i + hl;
            }
            halfLen[i] = hl;
        }

        // t 中回文子串的长度为 hl*2-1
        // 由于其中 # 的数量总是比字母的数量多 1
        // 因此其在 dfsStr 中对应的回文子串的长度为 hl-1
        // 这一结论可用在 isPalindrome 中

        // 判断左闭右开区间 [l,r) 是否为回文串  0<=l<r<=n
        // 根据下标转换关系得到 dfsStr 的 [l,r) 子串在 t 中对应的回文中心下标为 l+r+1
        // 需要满足 halfLen[l + r + 1] - 1 >= r - l,即 halfLen[l + r + 1] > r - l
        auto isPalindrome = [&](int l, int r) -> bool
        {
            return halfLen[l + r + 1] > r - l;
        };

        vector<bool> ans(n);
        for (int i = 0; i < n; i++)
        {
            ans[i] = isPalindrome(nodes[i].first, nodes[i].second);
        }
        return ans;
    }
};
// @lc code=end

复杂度分析

时间复杂度:O(n),其中 n 是字符串 s 的长度。

空间复杂度:O(n),其中 n 是字符串 s 的长度。

;