周赛传送门
又是两道题哎,不过下午补题补的很顺畅,大概是睡了一下午的缘故吧哈哈哈
5863. 统计特殊四元组
思路:哈希
时间复杂度:, 为 的取值范围。
设有二维数组 。 存储了 区间内 的数量。
可在 的时间复杂度内完成对 的初始化,代码如下:
int mark[51][101] = {0};
for (int i = nums.size()-1; i >= 0; i--) {
memcpy(mark[i], mark[i+1], sizeof(mark[i]));
mark[i][nums[i]]++;
}
接下来,枚举坐标 。设三个元素的累加和为 ,可通过累加 得到最终答案,完整代码如下:
class Solution {
public:
int countQuadruplets(vector<int>& nums) {
int mark[51][101] = {0};
for (int i = nums.size()-1; i >= 0; i--) {
memcpy(mark[i], mark[i+1], sizeof(mark[i]));
mark[i][nums[i]]++;
}
int anw = 0;
for (int i = 0; i < nums.size(); i++) {
for (int j = i+1; j < nums.size(); j++) {
for (int k = j+1; k < nums.size(); k++) {
int sum = nums[i] + nums[j] + nums[k];
if (sum <= 100) {
anw += mark[k+1][sum];
}
}
}
}
return anw;
}
};
5864. 游戏中弱角色的数量
思路:排序,双指针
时间复杂度:
首先将 按照 降序排序。
排序后,对于 ,可快速确定 高于 的最大下标,记为 。将区间 中的最高防御记为 。
如果 高于 的防御力,则 为弱角色,反之则不是。
因为 是 降序排序的,必有:
因此,可在 的时间复杂度内递推出所有的 以及 。
class Solution {
public:
int numberOfWeakCharacters(vector<vector<int>>& p) {
sort(p.begin(), p.end(), [](const auto &l, const auto &r) {
return l[0] > r[0];
});
int anw = 0;
for (int limit = 0, i = -1, j = 0; j < p.size(); j++) {
while (i+1 < j && p[i+1][0] != p[j][0]) {
i++;
limit = max(limit, p[i][1]);
}
if (limit > p[j][1]) {
anw++;
}
}
return anw;
}
};
5865. 访问完所有房间的第一天
思路:递推,拆点
时间复杂度:
考虑到每个房间有两种状态:
- 已被访问了奇数次
- 已被访问了偶数次
那不妨用两个数组 , 分别表示第一次和第二次到达房间的天数。
是很容易计算的:。
可拆解为三部分:
- 第一次到达房间 ,耗时 天。
- 从 回到 ,耗时 1 天。
- 从 再次回到 ,耗时 天。
前两部分比较好理解,不废话了。只说第三部分。
在回到 之后, 被访问了奇数次, 内的房间都被访问了偶数次。
不难发现, 内的房间的状态等价于第一次到达 时的状态,所以从 再次回到 的耗时为 。
综上所述:
class Solution {
public:
int firstDayBeenInAllRooms(vector<int>& next) {
vector<int64_t> f1(next.size());
vector<int64_t> f2(next.size());
f2[0] = 1;
const int64_t mod = 1e9+7;
for (int i = 1; i < next.size(); i++) {
f1[i] = (f2[i-1] + 1)%mod;
f2[i] = (f1[i] + 1 + (mod+f1[i] - f1[next[i]]))%mod;
}
return f1.back();
}
};
5866. 数组的最大公因数排序
思路:并查集,素数筛
时间复杂度:, 为取值范围。
设 表示排序之后 的位置,可在 的时间复杂度内得出,代码如下:
int cnt[100001] = {0};
int pos[30001] = {0};
// 统计每个数组出现的次数
for (auto n : nums) {
cnt[n]++;
}
// 计算 cnt 的前缀和
for (int i = 1; i <= 100000; i++) {
cnt[i] += cnt[i-1];
}
// 通过前缀和得出每个数组的位置
for (int i = 0; i < nums.size(); i++) {
pos[i] = --cnt[nums[i]];
}
现在问题变成了「位置 能否通过若干次变换到达位置 」。
考虑三个位置 ,,。如果 和 可交换, 和 可交换,那么借助 , 和 也可互换位置。熟悉并查集的老铁应该已经想到了,可以借助并查集维护位置之间的联通关系。
设有长度为 的数组 ,用于存储并查集:
- 前 个元素代表 中 个位置
- 后 个元素代表约数
因此可通过类似素数筛的枚举方式,找出可被素数 整除的位置,并将这些位置与 合并到一起。
最后,枚举 ,借助 判断 与 是否联通。若全部联通则返回 ,反之返回 。
class Solution {
public:
int fa[130001] = {0};
int find(int x) {
int t = x;
while(t != fa[t]) {
t = fa[t];
}
while(x != fa[x]) {
int tmp = fa[x];
fa[x] = t;
x = tmp;
}
return x;
}
void merge(int u, int v) {
int fu = find(u);
int fv = find(v);
fa[fu] = fv;
}
bool gcdSort(vector<int>& nums) {
int cnt[100001] = {0};
int pos[30001] = {0};
// 统计每个数组出现的次数
for (auto n : nums) {
cnt[n]++;
}
// 计算 cnt 的前缀和
for (int i = 1; i <= 100000; i++) {
cnt[i] += cnt[i-1];
}
// 通过前缀和得出每个数组的位置
for (int i = 0; i < nums.size(); i++) {
pos[i] = --cnt[nums[i]];
}
// 初始化并查集
for (int i = 0; i <= 130000; i++) {
fa[i] = i;
}
// 复用一下 cnt,cnt[i] 表示数字 i 第一次出现的位置
memset(cnt, -1, sizeof(cnt));
for (int i = 0; i < nums.size(); i++) {
if (cnt[nums[i]] != -1) {
// 相同数组必然是联通的,可以先合并
merge(cnt[nums[i]], i);
} else {
// 更新 cnt
cnt[nums[i]] = i;
}
}
// 素数筛
bool mark[100001] = {0};
for (int i = 2; i <= 100000; i++) {
if (mark[i]) { continue; }
mark[i] = true;
for (int j = i; j <= 100000; j += i) {
// j 在 nums 中出现过。显然 j 是能 i 整除的。
// 所以将 j 与 n+i 合并
if (cnt[j] != -1) {
merge(i+30000, cnt[j]);
}
}
}
// 判断 i 与 pos[i] 是否联通。
for (int i = 0; i < nums.size(); i++) {
if (find(i) != find(pos[i])) {
return false;
}
}
return true;
}
};