周赛 322 题解

plus2047 于 2022-12-04 发布

本周有两道图相关题目,可以复习一下图的最短路算法。

1.

image image

直接实现即可。

class Solution:
    def isCircularSentence(self, sentence: str) -> bool:
        words = sentence.split()
        return words[0][0] == words[-1][-1] and all(words[i][-1] == words[i + 1][0] for i in range(len(words) - 1))

2.

image

可以证明,如果合法的组对方式存在,最小数一定与最大数组对。否则,最大数跟任意其他数组成的数对之和,一定大于最小数与任意数组成的数对之和。然后,递归应用这一条推理,就能证明如果合法的组对方式存在,一定是第 i 小与第 i 大进行组队。之后实现就比较简单了。

class Solution:
    def dividePlayers(self, skill: List[int]) -> int:
        skill.sort()
        prod = 0
        total = skill[0] + skill[-1]
        for i in range(len(skill) // 2):
            if skill[i] + skill[- 1 - i] != total:
                return -1
            prod += skill[i] * skill[- 1 - i]
        return prod

3.

image image

这个问题我觉得是本次周赛最有意思的问题。谈到最短路算法,我们当然会首先想到 dijkstra 算法。但这个问题中总路径长度定义比较特殊,是整条路径中最短的一条子路径的长度。这个定义其实类似于负权路径,所以不能使用 dijkstra 算法,而是要使用类似于 bellman ford 算法。

这两个算法尽在实际实现时区别很小,主要是 dijkstra 使用优先队列,bellman ford 使用队列。但 dijkstra 算法会保证每个节点只进队列一次,bellman ford 却不能保证。这一点是理论分析结果,在代码上是不会有体现的。bellman ford 使用优先队列也能运行,这个问题上,使用优先队列会更快的找到最短路径,从而更快收敛。

# 这看上去是个 dijkstra, 实际上是个 bellman ford
def shortest(graph, from_node):
    import heapq
    node_num = len(graph)
    res = [float("inf")] * node_num
    heap = []
    # 队列中保存 (distance, node)
    # 注意初试节点 distance 需要设置为一个大数
    heapq.heappush(heap, (1e9, from_node))

    while len(heap):
        weight, node = heapq.heappop(heap)
        if weight >= res[node]:
            continue
        res[node] = weight
        for child in graph[node]:
            # 这里与 dijkstra 唯一的不同是求和改成 min
            heapq.heappush(heap, (min(weight, child[1]), child[0]))
    return res


class Solution:
    def minScore(self, n: int, roads: List[List[int]]) -> int:
        graph = [[] for _ in range(n + 1)]
        for x, y, d in roads:
            graph[x].append((y, d))
            graph[y].append((x, d))
        return shortest(graph, 1)[n]

4.

image image

这个问题其实并不复杂,只是操作有点繁琐。

很容易想到一个思路:从某一点出发,然后进行类似于 BFS 的层次遍历,遍历过程中检查是否合法,然后遍历的最大层数即为所求。这个思路成立的前提是,存在一种最优划分方法,其第一组只有一个节点。这一点是可以证明的。如果第一组有一个以上节点,只需要任意保留一个节点,将其他节点划分到第三组,则也是一个合法的划分。于是,我们可以从每个节点出发,层次遍历检查。这样时间复杂度是 O(n * num_edges) 在这个问题上时间复杂度有点紧张,保险起见使用 C++ 实现。

然后,需要注意这个题不保证图是联通的。所以我们需要分别求解每个子图。所以我们还得进行一遍子图划分,子图划分最简单的方法是使用并查集。最终代码如下。

// 并查集模板
struct MergeFindSet {
    std::vector<int> p;
    MergeFindSet(int n) : p(n) { init(); }
    int find(int x) { return p[x] == x ? x : p[x] = find(p[x]); }
    void merge(int root, int child) { p[find(child)] = find(root); }
    void init() {
        for (int i = 0; i < int(p.size()); i++) p[i] = i;
    }
};

class Solution {
public:
    int magnificentSets(int n, vector<vector<int>>& edges) {
        // 构建图和并查集
        vector<vector<int>> G(n + 1);
        MergeFindSet mfs(n + 1);
        for(auto& p: edges) {
            G[p[0]].push_back(p[1]);
            G[p[1]].push_back(p[0]);
            mfs.merge(p[0], p[1]);
        }
        
        // 每个连通子图的最大划分
        // 连通子图按照并查集集合需要划分,最大 n
        // -2 代表这个序号没有对应的子图
        // -1 代表这个子图无法分割
        vector<int> subg(n + 1, -2);
        
        // 尝试从每个节点出发进行层次遍历
        for(int i = 1; i <= n; i++) {
            int ng = bfs(G, i);
            // 按照子图在并查集中的序号更新结果
            int sub = mfs.find(i);
            subg[sub] = max(subg[sub], ng);
        }
        
        // 最终划分数量是所有子图划分数量之和
        // 但如果任意子图无法划分,则无法划分
        int res = 0;
        for(int x: subg) {
            if(x == -1) return -1;
            if(x == -2) continue;
            res += x;
        }
        return res;
    }

    // BFS 层次遍历
    int bfs(vector<vector<int>>& G, int node) {
        // 节点所属的 group
        vector<int> group(G.size(), -1);
        group[node] = 1;

        queue<int> q;        
        q.push(node);
        
        while(q.size()) {
            int curr = q.front();
            q.pop();
            int cg = group[curr];
            for(int child: G[curr]) {
                int cc = group[child];
                if(abs(cc - cg) == 1) continue;
                // 如果发现一个近临节点的 group 序号差异不是 1 则划分失败
                if(cc != -1) return -1;
                // 标记新的近临界点 group 序号
                group[child] = cg + 1;
                q.push(child);
            }
        }
        
        return *max_element(group.begin(), group.end());
    }
};