Leetcode力扣周赛 257

79 阅读2分钟

周赛传送门

又是两道题哎,不过下午补题补的很顺畅,大概是睡了一下午的缘故吧哈哈哈

5863. 统计特殊四元组

思路:哈希

时间复杂度O(n3+nlimit)O(n^3 + n*limit), limitlimitnumsnums 的取值范围。

设有二维数组 markmarkmarki,jmark_{i,j} 存储了 nums[i..n1]nums[i..n-1] 区间内 jj 的数量。

可在 O(nlimit)O(n*limit) 的时间复杂度内完成对 markmark 的初始化,代码如下:

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]]++;
}

接下来,枚举坐标 a,b,ca,b,c。设三个元素的累加和为 sumsum,可通过累加 markc+1,summark_{c+1, sum} 得到最终答案,完整代码如下:

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. 游戏中弱角色的数量

思路:排序,双指针

时间复杂度O(nlgn)O(n*\lg n)

首先将 propprop 按照 attackattack 降序排序。

排序后,对于 propiprop_i,可快速确定 attackattack 高于 propiprop_i 的最大下标,记为 pip_i。将区间 [0,pi][0, p_i] 中的最高防御记为 defidef_i

如果 defidef_i 高于 propiprop_i 的防御力,则 propiprop_i 为弱角色,反之则不是。

因为 proppropattackattack 降序排序的,必有:

  • pi1pip_{i-1} \le p_i
  • defi=max(defensepi,defi1)def_i = max(defense_{p_i}, def_{i-1})

因此,可在 O(n)O(n) 的时间复杂度内递推出所有的 pip_i 以及 defidef_i

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. 访问完所有房间的第一天

思路:递推,拆点

时间复杂度O(n)O(n)

考虑到每个房间有两种状态:

  • 已被访问了奇数次
  • 已被访问了偶数次

那不妨用两个数组 f1f1, f2f2 分别表示第一次和第二次到达房间的天数。

f1if1_i 是很容易计算的:f1i=f2i1+1f1_i = f2_{i-1} + 1

f2if2_i 可拆解为三部分:

  • 第一次到达房间 ii,耗时 f1if1_i 天。
  • ii 回到 nextinext_i,耗时 1 天。
  • nextinext_i 再次回到 ii,耗时 f1if1nextif1_i - f1_{next_i} 天。

前两部分比较好理解,不废话了。只说第三部分。

在回到 nextinext_i 之后,nextinext_i 被访问了奇数次,[nexti+1,i1][next_i+1, i-1] 内的房间都被访问了偶数次。

不难发现,[nexti,i1][next_i, i-1] 内的房间的状态等价于第一次到达 nextinext_i 时的状态,所以从 nextinext_i 再次回到 ii 的耗时为 f1if1nextif1_i - f1_{next_i}

综上所述:

  • f1i=f2i1+1f1_i = f2_{i-1} + 1
  • f2i=f1i+1+f1if1nextif2_i = f1_{i} + 1 + f1_{i} - f1_{next_i}
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. 数组的最大公因数排序

思路:并查集,素数筛

时间复杂度O(n+limitlimit)O(n+limit*\sqrt {limit})limitlimit 为取值范围。

posipos_i 表示排序之后 numsinums_i 的位置,可在 O(limit+n)O(limit+n) 的时间复杂度内得出,代码如下:

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]];
}

现在问题变成了「位置 ii 能否通过若干次变换到达位置 posipos_i」。

考虑三个位置 aabbcc。如果 aabb 可交换,bbcc 可交换,那么借助 bbaacc 也可互换位置。熟悉并查集的老铁应该已经想到了,可以借助并查集维护位置之间的联通关系。

设有长度为 n+limitn+limit 的数组 fafa,用于存储并查集:

  • nn 个元素代表 numsnumsnn 个位置
  • limitlimit 个元素代表约数

因此可通过类似素数筛的枚举方式,找出可被素数 pp 整除的位置,并将这些位置与 n+pn+p 合并到一起。

最后,枚举 i[0,n1]i∈[0,n-1],借助 fafa 判断 iiposipos_i 是否联通。若全部联通则返回 truetrue,反之返回 falsefalse

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;
    }
};