周赛 324 题解

plus2047 于 2022-12-18 发布

本周周赛有两道 Hard! 两道题难度中规中矩,分别是巨烦无比的分类讨论 & 暴力版本公共祖先。

1.

image image

非常简单的一道题目。

题目中其实定义了一种「相等」关系。对这类题目,我比较喜欢的一种 O(n) 时间复杂度的做法是,为每一类相等关系选取一个代表元作为 key, 然后数一下每个 key 出现的次数就行了。本题「代表元」可以选取将 word 所有字母去重排序后的字符串。

class Solution:
    def similarPairs(self, words: List[str]) -> int:
        # 计数每个代表元所在的彼此相等集合中元素个数
        cnt = collections.Counter("".join(sorted(set(s))) for s in words)
        # 每个集合提供的 pair 是 x * (x - 1) / 2
        return sum(x * (x - 1) // 2 for x in cnt.values())

2.

image

这是一个很不像 LeetCode 风格的「数论」题目,你需要了解质因数分解算法。这里直接给出一个比较快的质因数分解算法模板。由于这个模板是 CPP 的,这道题目就 CPP 了。

质因数分解是 O(n ^ 0.5) 的时间复杂度。这道题目每次操作直觉上会令 n 的值以接近 log n 的程度衰减(好像不难证明,留给读者试试),所以这个平方根时间复杂度的质因数分解算法 + 暴力迭代就够了。

// worst complexity: O(num ^ 0.5)
template <typename NUM = int>
void prime_fact(NUM num, vector<NUM>& res) {
    while (num % 2 == 0) {
        num /= 2;
        res.push_back(2);
    }
    for (int p = 3; p * p <= num; p += 2) {
        while (num % p == 0) {
            res.push_back(p);
            num /= p;
        }
    }
    if (num > 1) res.push_back(num);
}

class Solution {
public:
    int smallestValue(int n) {
        vector<int> fact;
        while(true) {
            fact.clear();
            prime_fact(n, fact);
            int _n = accumulate(fact.begin(), fact.end(), 0);
            if(_n == n) break;
            n = _n;
        }
        return n;
    }
};

3.

image image

这是一道我最讨厌的题目,仔细看了好多遍,好像确实不难,但也没啥漂亮解法,是一个比较麻烦的分类讨论,要写很多 if else.

基本思路是,原图中最多可以有四个度数为奇数的节点,然后根据节点个数展开分类讨论。在尝试加入新的边时需要检查是否重复,所以需要把所有的边加到一个 set 里。

class Solution {
public:
    bool isPossible(int n, vector<vector<int>>& edges) {
        // 统计度数
        vector<int> degree(n + 1);
        // 边 set, 用于检查边是否已经存在
        // 总是把边较小的节点放在前面
        set<pair<int, int>> es;
        for(auto& p: edges) {
            degree[p[0]]++;
            degree[p[1]]++;
            es.insert({min(p[0], p[1]), max(p[0], p[1])});
        }
        // 所有度数为奇数的候选节点
        vector<int> can;
        for(int i = 1; i <= n; i++) {
            if(degree[i] % 2) {
                can.push_back(i);
            }
        }
        // 没有度数为奇数的节点,无需操作
        if(can.size() == 0) {
            return true;
        }
        if(can.size() == 2) {
            // 两个奇数度数节点
            // 如果这两个节点之间没有变,则加一条边即可
            if(!es.count({can[0], can[1]})) {
                return true;
            }
            // 否则,需要另找一个节点,然后分别跟这个节点加一条边
            // 所以需要 can 中两个节点跟这个节点都没有边相连
            for(int j = 1; j <= n; j++) {
                if(j != can[0] and j != can[1]) {
                    auto p1 = make_pair(min(can[0], j), max(can[0], j));
                    auto p2 = make_pair(min(can[1], j), max(can[1], j));
                    if(!es.count(p1) and !es.count(p2)) {
                        return true;
                    }
                }
            }
        } else if(can.size() == 4) {
            // 四个奇数节点,则必须能够两两加一条边
            if(
                (!es.count({can[0], can[1]}) and !es.count({can[2], can[3]})) or
                (!es.count({can[0], can[2]}) and !es.count({can[1], can[3]})) or
                (!es.count({can[0], can[3]}) and !es.count({can[1], can[2]}))
            ) {
                return true;
            }
        }
        // 其他情况都不行
        return false;
    }
};

4.

image image

第四题难度感觉比第三题难度小一点。要求的环的长度,其实就等于这两个节点到「最近公共祖先」距离之和加一。由于这是一棵满树,高度最多 30, 所以可以暴力求解最近公共祖先。而且这个满树可以根据节点编号方便的求出节点深度,从而很容易判断两个节点是不是在同一层。

class Solution {
public:
    vector<int> cycleLengthQueries(int n, vector<vector<int>>& queries) {
        vector<int> res;
        for(auto &p: queries) {
            int x = min(p[0], p[1]), y = max(p[0], p[1]);
            int r = 1;
            // y 编号总是大于 x,所以层数总是比 x 更深
            // 如果 y 与 x 不在同一层,则先把 y 提升到跟 x 同一层
            // '>>= 1' 即父节点
            while(level(y) > level(x)) {
                y >>= 1;
                r++;
            }
            // 两者在同一层,则一直上移直到到达公共祖先
            while(x != y) {
                y >>= 1;
                x >>= 1;
                r += 2;
            }
            res.push_back(r);
        }
        return res;
    }
    // 求节点层数,观察一下不难发现,节点层数也即节点编号的最高有效位位置
    int level(int x) {
        for(int m = 1; m <= 30; m++) {
            if(x < (1 << m)) {
                return m;
            }
        }
        return 31;
    }
};