Skip to content

E. Graph Composition - 题解

比赛与标签

比赛: Codeforces Round 998 (Div. 3) 标签: dfs and similar, dsu, graphs, greedy 难度: *1500

题目大意喵~

主人 sama,下午好呀!这道题是这样的呐:

我们有两个都包含 n 个顶点的图,分别叫做 FG。图 Fm1 条边,图 Gm2 条边。我们可以对图 F 进行两种操作,次数不限:

  1. 删除边:在 F 中选择一条存在的边 (u, v),然后把它删掉。
  2. 添加边:在 F 中选择两个没有边直接相连的顶点 uv,然后给它们加上一条边。

我们的目标是,通过最少的操作次数,让图 F 和图 G连通性 完全一样。也就是说,对于任意两个顶点 uv,如果它们在 F 中是连通的(存在路径),那么它们在 G 中也必须是连通的;反之亦然。

我们要做的就是计算出这个最少的操作次数是多少,喵~

解题思路分析喵~

这道题的目标是让两个图的“连通性”一致。这个词听起来有点抽象,但其实它有一个非常具体的含义哦!两个顶点连通,就是说它们在同一个 连通分量 里。所以,题目的目标可以翻译成:通过最少的操作,让图 F 的连通分量划分和图 G 的连通分量划分完全相同

既然是连通分量的问题,那本喵的 DNA 动了!并查集 (DSU) 就是解决这类问题的神器呀!

我们可以把整个问题拆解成两个独立的部分来考虑:

1. "错误"的边必须删除!(计算删除代价)

首先,我们来看看 G 的连通分量是什么样的。我们可以用并查集 dsu_g 跑一遍 G 的所有边,就能得到 G 的所有连通分量了。

现在,考虑 F 中的任意一条边 (u, v)

  • 如果 uvG 中本来就在同一个连通分量里,那么这条边在 F 中是“可能有用”的,因为它维持了 G 中本应有的连通性。
  • 但如果 uvG 中属于 不同 的连通分量,那么 F 中的这条边 (u, v) 就是一条“错误”的边!它把两个在 G 中本不应该连通的集合给连起来了。为了让 F 的连通分量和 G 一致,这条边 必须被删除

所以,第一步的删除代价就是 F 中所有连接了不同 G-连通分量的边的数量。我们把这些必须删除的边去掉,剩下的就是“合法”的边了,我们称之为 valid_f_edges

2. "缺失"的连接需要补上!(计算添加代价)

经过第一步,我们已经把所有“错误”的边都删掉了。现在 F 中只剩下 valid_f_edges。这些边连接的顶点,在 G 中都属于同一个连通分量。

但是,这样就够了吗?不一定哦!

考虑 G 中的一个连通分量 C_gC_g 里的所有顶点都应该在最终的 F 图里也互相连通。但是,valid_f_edges 可能不足以将 C_g 里的所有顶点都连接起来。它们可能把 C_g 划分成了好几个更小的连通块。

举个例子喵:G{1, 2, 3, 4} 是一个连通分量。而 valid_f_edges 中只有一条边 (1, 2)。那么在当前的 F 中,{1, 2} 是一个连通块,{3} 是一个连通块,{4} 是一个连通块。C_g 被分成了 3 个小块。

要把 k 个独立的连通块合并成一个大的连通块,最少需要添加 k-1 条边,把它们像串糖葫芦一样串起来~

所以,第二步的添加代价就是:对于 G 的每一个连通分量,我们计算它被 valid_f_edges 划分成了多少个小连通块(假设是 k 个),然后需要添加 k-1 条边。把所有 G-连通分量的添加代价加起来,就是总的添加代价。

怎么计算这个 k 呢?我们可以再用一个并查集 dsu_f_valid

  1. valid_f_edges 来构建 dsu_f_valid
  2. 遍历所有顶点 1n。对于每个顶点 i,我们知道它属于哪个 G-连通分量(通过 dsu_g.find(i))和哪个由 valid_f_edges 形成的 F-连通块(通过 dsu_f_valid.find(i))。
  3. 我们可以用一个 map<int, set<int>> 来记录。map 的键是 G-连通分量的代表元,值是一个 set,存放这个 G-连通分量内包含了哪些 F-连通块的代表元。
  4. 最后,遍历这个 map,对于每个 G-连通分量,它对应的 set 的大小 k 就是它被分成的块数,我们就需要 k-1 次添加操作。

总结一下思路:

  1. 用并查集 dsu_g 确定 G 的连通分量。
  2. 遍历 F 的边,如果一条边连接了两个不同的 G-连通分量,则计入删除代价。保留下“合法”的边 valid_f_edges
  3. 用另一个并查集 dsu_f_valid 处理 valid_f_edges,得到由它们构成的连通块。
  4. 统计每个 G-连通分量内,包含了多少个 dsu_f_valid 形成的连通块。如果包含 k 个,则需要 k-1 次添加操作。累加得到总的添加代价。
  5. 最终答案 = 删除代价 + 添加代价。

这两个过程是独立的,所以分开计算再相加,得到的就是最少操作次数,完美~

代码实现喵~

cpp
#include <iostream>
#include <vector>
#include <numeric>
#include <set>
#include <map>
#include <algorithm>

// 一个非常标准的并查集 (DSU) 结构,用来处理连通分量问题,喵~
struct DSU {
    std::vector<int> parent;
    std::vector<int> sz; // 用来做按大小合并的优化,效率更高!
    DSU(int n) {
        parent.resize(n + 1);
        std::iota(parent.begin(), parent.end(), 0); // 初始化,每个节点的父亲都是自己
        sz.assign(n + 1, 1); // 每个集合初始大小为 1
    }

    // 查找一个元素的根节点(代表元),带路径压缩优化
    int find(int i) {
        if (parent[i] == i)
            return i;
        return parent[i] = find(parent[i]);
    }

    // 合并两个元素所在的集合
    void unite(int i, int j) {
        int root_i = find(i);
        int root_j = find(j);
        if (root_i != root_j) {
            // 按大小合并,小树合并到大树上
            if (sz[root_i] < sz[root_j])
                std::swap(root_i, root_j);
            parent[root_j] = root_i;
            sz[root_i] += sz[root_j];
        }
    }
};

void solve() {
    int n;
    long long m1, m2;
    std::cin >> n >> m1 >> m2;

    std::vector<std::pair<int, int>> edges_f(m1);
    for (int i = 0; i < m1; ++i) {
        std::cin >> edges_f[i].first >> edges_f[i].second;
    }

    std::vector<std::pair<int, int>> edges_g(m2);
    for (int i = 0; i < m2; ++i) {
        std::cin >> edges_g[i].first >> edges_g[i].second;
    }

    // 步骤 1: 使用 DSU 找到图 G 的所有连通分量
    DSU dsu_g(n);
    for (const auto& edge : edges_g) {
        dsu_g.unite(edge.first, edge.second);
    }

    // 步骤 2: 识别 F 中“合法”的边,并计算删除代价
    // “合法”的边是指连接的两个顶点在 G 中也属于同一个连通分量
    std::vector<std::pair<int, int>> valid_f_edges;
    valid_f_edges.reserve(m1); // 预分配内存,小优化~
    for (const auto& edge : edges_f) {
        if (dsu_g.find(edge.first) == dsu_g.find(edge.second)) {
            valid_f_edges.push_back(edge);
        }
    }
    // 删除代价 = F 的总边数 - 合法边数
    long long removal_cost = m1 - valid_f_edges.size();

    // 步骤 3: 使用另一个 DSU 处理这些“合法”的 F 边,找到它们构成的连通块
    DSU dsu_f_valid(n);
    for (const auto& edge : valid_f_edges) {
        dsu_f_valid.unite(edge.first, edge.second);
    }

    // 步骤 4: 计算添加代价
    // 对于 G 的每个连通分量,看它被合法的 F 边分成了多少个小块
    std::map<int, std::set<int>> g_comp_to_f_valid_comps;
    for (int i = 1; i <= n; ++i) {
        // G 中 i 所在的连通分量的代表元
        int g_root = dsu_g.find(i);
        // 合法 F 边构成的图中 i 所在的连通块的代表元
        int f_valid_root = dsu_f_valid.find(i);
        // 记录下来:这个 G-分量 包含了那个 F-块
        g_comp_to_f_valid_comps[g_root].insert(f_valid_root);
    }

    long long addition_cost = 0;
    // 遍历所有 G-连通分量
    for (const auto& pair : g_comp_to_f_valid_comps) {
        // pair.second.size() 就是这个 G-分量被分成的块数 k
        // 需要 k-1 条边来把它们连起来
        addition_cost += pair.second.size() - 1;
    }

    // 步骤 5: 总代价 = 删除代价 + 添加代价
    std::cout << removal_cost + addition_cost << "\n";
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    int t;
    std::cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}

复杂度分析的说

  • 时间复杂度: O((n + m1 + m2) * α(n)) 的说。
    • 这里的 α(n) 是阿克曼函数的反函数,它增长得非常非常慢,对于我们遇到的所有实际情况,都可以把它看作一个很小的常数(比如小于5)。
    • 构建 dsu_g 需要遍历 m2 条边,每次 unite 操作的平摊时间是 O(α(n))。总共是 O(m2 * α(n))
    • 遍历 m1F 的边来计算删除代价和筛选合法边,需要 O(m1 * α(n))
    • 构建 dsu_f_valid 需要遍历 valid_f_edges(最多 m1 条),需要 O(m1 * α(n))
    • 统计添加代价时,需要遍历 n 个顶点和 mapmap 的大小最多是 n。总共是 O(n * α(n) + n log n)(因为 set 的插入),但由于代表元数量有限,这里可以更精细地分析,不过大致上是线性的。总体来看,整个算法的瓶颈在于处理边和点,所以是线性的。
  • 空间复杂度: O(n + m1 + m2) 的说。
    • 两个并查集都需要 O(n) 的空间。
    • 存储 FG 的边列表需要 O(m1 + m2) 的空间。
    • mapset 最多存储 n 个元素,所以也是 O(n) 的空间。
    • 所以总空间是线性的。

知识点与总结喵!

这道题真是一道非常经典的并查集应用题呢,把一个看似复杂的问题分解得明明白白!

  1. 核心思想 - 问题分解: 最重要的技巧就是把“让连通性一致”这个目标分解为两个独立的子问题:删除所有不该存在的连接补全所有应该存在的连接。这种化繁为简的能力在算法竞赛中超级重要!

  2. 并查集的双重应用: 我们用了两次并查集,但目的不同!

    • 第一次 (dsu_g) 是为了建立一个“标准答案”,即 G 的连通性是怎样的。
    • 第二次 (dsu_f_valid) 是为了分析在遵守“标准答案”的前提下,我们手头已有的资源(合法的 F 边)能做到什么程度。
  3. 图论基础: 理解“连通分量”的本质,以及“用 k-1 条边连接 k 个连通块”这个最基本的图论性质是解题的关键。

  4. 编程技巧: 使用 std::map<Key, std::set<Value>> 来统计分组信息是一个非常实用的技巧,可以清晰地处理“一个大集合里有多少个不同的小集合”这类问题。

希望本喵的讲解对主人 sama 有帮助哦!遇到图论问题不要怕,先想想它最核心的概念是什么,很多时候问题就会迎刃而解啦!加油喵~!

Released under the MIT License.