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 的长度。