大家好,我是梁唐。
照惯例我们来看下昨天的LeetCode周赛。这一场是LeetCode第310场,由趋势科技赞助举办。
其中1-5名可以直通面试……
我们来看看欢乐的评论区……
出现最频繁的偶数元素
给你一个整数数组 nums
,返回出现最频繁的偶数元素。
如果存在多个满足条件的元素,只需要返回 最小 的一个。如果不存在这样的元素,返回 -1
。
题解
水题,统计一下所有偶数出现的次数,找到其中出现次数最多并且最小的即可。
class Solution {
public:
int mostFrequentEven(vector<int>& nums) {
int ret = -1, maxi = 0;
map<int, int> mp;
for (auto &x : nums) {
if (x % 2) continue;
mp[x]++;
if (mp[x] > maxi || (mp[x] == maxi && x < ret)) {
ret = x;
maxi = mp[x];
}
}
return ret;
}
};
复制代码
子字符串的最优划分
给你一个字符串 s
,请你将该字符串划分成一个或多个 子字符串 ,并满足每个子字符串中的字符都是 唯一 的。也就是说,在单个子字符串中,字母的出现次数都不超过 一次 。
满足题目要求的情况下,返回 最少 需要划分多少个子字符串*。*
注意,划分后,原字符串中的每个字符都应该恰好属于一个子字符串。
题解
注意,题目当中说的是子字符串,而非子序列,子字符串需要保证连续。
既然要保证连续,那么当然是在划分的时候尽可能多地包含尽量多的字符。所以这是一道明显的贪心题。
我们可以使用set
来存储当前分割的子字符串,当遇到重复字符时意味着需要新分割一个子串,我们用清空set
的操作来代替新创建子串。维护一下创建新子串的次数即为答案。
class Solution {
public:
int partitionString(string s) {
int n = s.length();
int ret = 0;
set<char> st;
for (int i = 0; i < n; i++) {
if (st.count(s[i])) {
ret++;
st.clear();
}
st.insert(s[i]);
}
return ret+1;
}
};
复制代码
将区间分为最少组数
给你一个二维整数数组 intervals
,其中 intervals[i] = [lefti, righti]
表示 闭 区间 [lefti, righti]
。
你需要将 intervals
划分为一个或者多个区间 组 ,每个区间 只 属于一个组,且同一个组中任意两个区间 不相交 。
请你返回 最少 需要划分成多少个组。
如果两个区间覆盖的范围有重叠(即至少有一个公共数字),那么我们称这两个区间是 相交 的。比方说区间 [1, 5]
和 [5, 8]
相交。
题解
这是一道区间覆盖问题,我们当然可以用线段树来区间操作,当然仔细想想其实也能发现更简单的解法。
我们分析题意会发现,同一个分组内的区间不能互相重叠。也就意味着如果存在多个区间互相之间有重叠,它们需要被划分进不同的分组当中。为了方便发现区间重叠的情况,我们可以对区间进行排序。维护每一个分组的结束位置,如果下一个区间的开始位置在它之前,那么说明它们会发生重叠,我们需要新开辟一个分组。
那么,我们怎么知道有没有一个分组可以容纳当前这个区间呢?答案也很简单,我们选择结束位置最早的分组。因为所有区间都是按照从小到大顺序排序的,如果连结束得最早的分组都无法兼容,那么其他分组也一样无法兼容。
我们可以使用一个优先队列来维护每一个分组结束的时间,队列头部的元素,就是最早结束的分组下标。
class Solution {
public:
int minGroups(vector<vector<int>>& inte) {
sort(inte.begin(), inte.end());
priority_queue<int, vector<int>, greater<int>> pq;
for (auto &vt: inte) {
int s = vt[0], e = vt[1];
// 如果队列非空,并且最早结束的分组能够兼容
if (!pq.empty() && pq.top() < s) {
// 弹出,即更新该分组的结束位置
pq.pop();
}
pq.push(e);
}
// 队列中的元素数量即为答案
return pq.size();
}
};
复制代码
最长递增子序列 II
给你一个整数数组 nums
和一个整数 k
。
找到 nums
中满足以下要求的最长子序列:
- 子序列 严格递增
- 子序列中相邻元素的差值 不超过
k
。
请你返回满足上述要求的 最长子序列 的长度。
子序列 是从一个数组中删除部分元素后,剩余元素不改变顺序得到的数组。
题解
曾经有一道经典的动态规划例题,叫做最长不下降子序列。其实这两题的解法一致,我们也需要维护数组当中的每一个元素,以它为结尾的最长子序列的长度。
比如元素nums[i]
,如果我们求出了它之前所有位置的最长子序列的长度。那么我们只需要遍历0到i为止的所有元素,找到满足nums[i]-k<=nums[j]<nums[i]
的长度最大的j,就可以知道当前nums[i]
对应的最长子序列的长度。
这样的解法是OK的,但是有一个比较大的问题:这样做时间复杂度太大,对于每一个位置我们都要遍历它之前所有的元素。时间复杂度是。本题的数据量级是1e5,在这样的复杂度下是无法通过的。
我们可以进一步思考,对于nums[i]
来说,我们其实只关心在[nums[i]-k, nums[i])
这个半闭半开区间内元素对应的子序列长度的最大值。这是一个经典的区间求最值的问题,是经典的单点更新,区间查找的线段树的使用场景。
熟悉线段树的同学相信很容易即可写出代码,不熟悉的同学可能需要去学习一下线段树这个数据结构。
class Solution {
public:
// 数组存储线段树中所有节点
int arr[300500] {0};
const int N = 100050;
// k是线段树节点下标,它存储[l, r)区间内最大值,查询区间为[ll, rr)
int query(int k, int l, int r, int ll, int rr) {
if (ll <= l && rr >= r) {
return arr[k];
}
if (rr <= l || ll >= r) {
return 0;
}
int m = (l + r) >> 1;
if (rr <= m) return query(k << 1, l, m, ll, rr);
if (ll >= m) return query(k << 1 | 1, m, r, ll, rr);
return max(query(k << 1, l, m, ll, rr), query(k << 1 | 1, m, r, ll, rr));
}
// 将节点p更新成v
void update(int k, int l, int r, int p, int v) {
arr[k] = max(arr[k], v);
if (l+1 >= r) return ;
int m = (l + r) >> 1;
if (p < m) update(k << 1, l, m, p, v);
if (p >= m) update(k << 1 | 1, m, r, p, v);
}
int lengthOfLIS(vector<int>& nums, int k) {
int n = nums.size();
int ret = 0;
for (int i = 0; i < n; i++) {
int v = query(1, 0, N, nums[i] - k, nums[i]) + 1;
update(1, 0, N, nums[i], v);
ret = max(ret, v);
}
return ret;
}
};
复制代码