Codeforces Round 123 (Div. 2) E. Building Forest(带权并查集)

原题链接:E. Building Forest


题目大意:


给定 n n n 次操作,每次操作要操作 k k k 次。

对于第 i i i 次操作而言:

  • 先给出一个数字 k k k ,之后跟着 2 k 2k 2k 个数字: v 1 , w 1 , v 2 , w 2 . . . v k , w k v_1,w_1,v_2,w_2...v_k,w_k v1,w1,v2,w2...vk,wk
  • 对于每一组 v j , k j ( 1 ≤ j ≤ k ) v_j,k_j(1 \leq j \leq k) vj,kj(1jk) 而言,代表着从 v j v_j vj 出发,到达 v j v_j vj 所在连通块的根节点(设为 r o o t root root ),并且让该根节点对点 i i i 连上一条权值为 d i s t ( v j , r o o t ) + w j dist(v_j,root)+w_j dist(vj,root)+wj 的单向边,其中 d i s t ( u , v ) dist(u,v) dist(u,v) 代表着 u → v u \rightarrow v uv 的最短路长度。
  • 保证所加的边不会有自环和重边,且不会形成环(即一定会是一张 D A G DAG DAG 图)。

在你执行完所有操作后,问你这一张 D A G DAG DAG 图上的所有边权的权值之和是多少,输出这个权值和对 1 0 9 + 7 10^{9}+7 109+7 取模的值。

解题思路:


看到一眼就想到了带权并查集。

我们引入一个数据结构。

带权并查集

这个并查集维护的是点的连通性,以及集合中任意两点之间的距离(注意,这个"距离"并不一定代表着原图两点的距离,根据合并情况的不同,代表的信息会是不一样的),并查集能做到的带权并查集也能做到,带权并查集事实上就是并查集的另一扩展。

带权并查集事实上是将一些要维护的信息作为"边权",这些信息构成的内容变成"距离",然后经过合并集合后我们就可以高效的查询集合内的信息了。

下面讲解一下带权并查集维护信息的方式。

路径压缩

我们考虑一下在并查集路径压缩时,如果带上边权,我们的权值应该怎么样来进行维护。

假设我们的边权为 w w w ,距离数组 d i s u dis_u disu 代表点 u u u 到并查集的根节点的距离,我们来考虑下图的信息合并。

在这里插入图片描述

假设我们最初时有两个分别以 1 , 2 1,2 1,2 为根的集合,我们将其合并了,但我们现在想把节点 3 3 3 的路径压缩到根节点 1 1 1 上,考虑如何进行合并。

我们将每个边带上边权,我们最初时的状态是这样的:

在这里插入图片描述
此时我们要把集合 2 2 2 合并到集合 1 1 1 上,并且这一条边的边权权值为 3 3 3

我们的 d i s u dis_u disu 代表的是点 u u u 到达该点的根节点 r o o t root root 的距离,那么显然,我们的 d i s 2 = 0 → d i s 2 = 3 dis_2 = 0 \rightarrow dis_2 = 3 dis2=0dis2=3 ,这样我们的 d i s 2 dis_2 dis2 就维护好了,此时我们两个集合的根节点就变成了 1 1 1

在这里插入图片描述
此时我们就只有 d i s 3 dis_3 dis3 没有向上压缩路径了,考虑如何将其合并到节点 1 1 1 上 。

注意到一个问题,我们在合并集合时,由于我们使用的是递归式合并,我们执行合并时一定都是两个集合进行合并。并且其内部的所有点都会合并到其对应的根节点上,不会出现类似链状的情况。

在这里插入图片描述
那么我们递归向上进行合并时,我们的父节点一定会与我们所在集合其对应的根节点 1 1 1 相连,那么我们一定会转化到下面这种情况:

在这里插入图片描述
那么此时我们的 3 3 3 想要与 1 1 1 直接相连,那么我们的边权就显而易见了:

d i s 3 = d i s 2 + d i s 3 \begin{array}{c} dis_3=dis_2+dis_3 \end{array} dis3=dis2+dis3

在这里插入图片描述
这样子,我们的路径压缩就成功了。


    function<int(int)> find = [&](int x) {
        if (x == fa[x]) return x;
        find(fa[x]); dis[x] += dis[fa[x]];//权值加上父亲的距离
        return fa[x] = fa[fa[x]];
    };

集合合并

假设我们最初时有两个分别以 1 , 2 1,2 1,2 为根的集合,现在我想让两个集合相互连通,同时也要知道联通后这两个集合中任意两点的距离,考虑怎么维护这个信息。
在这里插入图片描述

(首先强调,这里的边并不一定代表着单向边,笔者为了方便标记,图中的这些单向边的意义是指出表示一个点的根节点,而在带权并查集中这些单向边应该是双向边)

假设我们让点 3 3 3 和点 4 4 4 连上一条边,权值为 4 4 4 ,考虑要如何把两个集合合并。首先我们肯定要将边 ( 2 , 3 ) (2,3) (2,3) 其转化到 ( 1 , 2 ) (1,2) (1,2) 上来进行合并,才能保证合并不会破坏并查集的结构。

那么这一条边权会是多少呢?

在这里插入图片描述
首先先说明一个定义,在所有路径都已经被压缩的情况下,我们的 d i s u dis_u disu 数组代表的是:我们从点 u u u 出发到达点 r o o t root root ,经过的所有"边权"的权值之和为 d i s u dis_u disu

那么对于上图来说,我们要连上 2 → 1 2 \rightarrow 1 21 这条边,其权值便代表的是新的 d i s 2 dis_2 dis2 的值,即 d i s t ( 2 , 1 ) dist(2,1) dist(2,1) 的值。

又因为我们是通过 ( 3 , 4 ) (3,4) (3,4) 这一条边将两个集合相连的,所以要我们会从点 2 2 2 出发,经过这一条边再到达点 1 1 1 ,那么这一条边的权值就是。

d i s 2 = d i s 4 + w − d i s 3 \begin{array}{c} dis_2=dis_4+w-dis_3 \end{array} dis2=dis4+wdis3

还有要注意的一点就是,根据连边的情况不同,合并后所代表的信息同时也会不同,因此要注意合并的顺序。

    bool merge(int x, int y, i64 w) {
        int xf = find(x), yf = find(y);
        if (xf == yf) return false;//如果已经在一个集合内 则不合并
        siz[yf] += siz[xf]; fa[xf] = yf;//否则改变根节点的父节点
        dis[xf] = -dis[x] + w + dis[y];//改变根节点的边权
        return true;
    }

查询距离

既然我们以及将集合合并成为了另一个集合:

从下图:
在这里插入图片描述
变为:
在这里插入图片描述

现在我们要询问两个点之间的"距离"?

  • 我们要查询 d i s t ( 1 , 2 ) dist(1,2) dist(1,2) 的值,那么答案即为 d i s 1 − d i s 4 dis_1 - dis_4 dis1dis4
  • 我们要查询 d i s t ( 3 , 2 ) dist(3,2) dist(3,2) 的值,那么答案即为 d i s 3 − d i s 4 dis_3 - dis_4 dis3dis4
  • 我们要查询 d i s t ( 2 , 3 ) dist(2,3) dist(2,3) 的值,那么答案即为 d i s 2 − d i s 3 dis_2 - dis_3 dis2dis3

同理,询问根据查询的不同也会有所不同。

    i64 dist(int x, int y) {
        int xf = find(x), yf = find(y);
        if (xf != yf) return -1;
        return abs(dis[x] + -dis[y]);
    }

回到本题:

题目要求的是点 v j v_j vj 到达根节点 r o o t root root 的距离 d i s t ( v 1 , r o o t ) + w j dist(v1,root)+w_j dist(v1,root)+wj,考虑如何用带权并查集维护这个问题。

注意到题目的特殊性质:

让该根节点对点 i i i 连上一条权值为 d i s t ( v j , r o o t ) + w j dist(v_j,root)+w_j dist(vj,root)+wj 的单向边

如果我们让边权的信息为原图的边权,距离的信息为原图中 v v v 到达 r o o t root root 的距离,且按照题中的连边方式的话,我们查询的信息 d i s t ( v , r o o t ) dist(v,root) dist(v,root) 是能正确被维护出来的。

但是任取两点 d i s t ( u , v ) dist(u,v) dist(u,v) 得到的距离是不对的,这个可以自己思考一下。

那么我们直接按照带权并查集的过程模拟操作即可。

时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn)(事实上不是 log ⁡ n \log n logn 的复杂度)

AC代码


#include <bits/stdc++.h>
#define YES return void(cout << "Yes\n")
#define NO return void(cout << "No\n")
using namespace std;

using ui64 = unsigned long long;
using PII = pair<int, int>;
using i64 = long long;

const int mod = 1e9 + 7;

struct DSU2 {
    vector<int> fa, siz;
    vector<i64> dis;
    DSU2(int n) : fa(n + 1), siz(n + 1, 1), dis(n + 1) { iota(fa.begin(), fa.end(), 0); };
    function<int(int)> find = [&](int x) {
        if (x == fa[x]) return x; find(fa[x]);
        (dis[x] += dis[fa[x]]) %= mod;
        return fa[x] = fa[fa[x]];
    };
    int size(int x) { return siz[find(x)]; }
    i64 dist(int x, int y) {
        int xf = find(x), yf = find(y);
        if (xf != yf) return -1;
        return dis[x] + -dis[y];
    }
    bool same(int x, int y) { return find(x) == find(y); }
    bool merge(int x, int y, i64 w) {
        int xf = find(x), yf = find(y);
        if (xf == yf) return false;
        siz[yf] += siz[xf]; fa[xf] = yf;
        (dis[xf] = -dis[x] + w + dis[y]) %= mod;
        return true;
    }
};

void solve() {
    int n;
    cin >> n;

    DSU2 dsu(n);

    i64 ans = 0;
    for (int i = 1; i <= n; ++i) {
        int x; cin >> x;
        for (int j = 1; j <= x; ++j) {
            i64 v, w;
            cin >> v >> w;
            int root = dsu.find(v);
            //注意查询的顺序 v -> root 的距离
            i64 W = (dsu.dist(v, root) + w) % mod;
            //注意连边的顺序 root -> i 权值为 w
            dsu.merge(root, i, W);
            (ans += W) %= mod;
        }
    }

    cout << (ans + mod) % mod << '\n';
}

signed main() {

    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int t = 1; //cin >> t;
    while (t--) solve();

    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: "Building Java Programs"是一本非常受欢迎的Java编程教材,适合初学者和有一定编程基础的人。这本书以清晰易懂的方式解释Java编程的核心概念和技术,并提供了大量的练习和示例来帮助读者巩固所学知识。 在"Building Java Programs"中,作者以渐进的方式引导读者学习Java编程。从基础的语法和数据类型开始,到控制流、方法、数组和对象等更高级的概念,每一章都有详细的解释和实例。书中还包括了用于处理文件、异常处理、继承和多态等更高级主题的章节。 这本书的一个重要特点是它的编程风格。作者强调编写清晰、模块化和可重用的代码,并提倡良好的编码实践。书中有很多习题和项目,可以帮助读者理解这些原则并将它们应用到实际的编程中。 "Building Java Programs"还介绍了Java的标准类库,这些类库提供了很多现成的工具和功能,可以简化编程过程。读者将学习如何使用类库中的类和方法来解决各种编程问题。 总之,"Building Java Programs"这本书以易懂和循序渐进的方式教授Java编程,适合想要学习Java或提高Java编程技能的人使用。无论是初学者还是有一定经验的开发者,都能从中受益,建立坚实的Java编程基础。 ### 回答2: "Building Java Programs" 是指使用Java编程语言构建程序的过程。在这个过程中,我们使用Java的语法、命令和库来设计、开发和测试程序。 首先,我们需要了解Java编程语言的基本知识和概念。Java是一种面向对象的编程语言,它具有丰富的类库和功能,可以用于开发各种应用程序,从简单的控制台应用到复杂的桌面应用和Web应用。 在构建Java程序时,我们需要有清晰的程序设计和逻辑思维能力。我们需要分析问题的需求,并将其转化为计算机可以理解和执行的步骤。这需要我们理解算法、数据结构和编程范式等概念。 接下来,我们使用Java的开发环境(如Eclipse或IntelliJ IDEA)创建一个Java项目。在项目中,我们可以创建多个包和类来组织代码。每个类都可以有成员变量、方法和构造函数。我们使用类来封装数据和行为,实现模块化和重用。 在编写Java程序时,我们需要遵循Java的语法和规范。我们需要理解变量、运算符、条件语句和循环等基本语法元素。我们还需要了解类、对象、继承、多态和接口等面向对象编程的概念。 在程序开发过程中,我们需要进行调试和测试。我们可以使用调试工具来跟踪程序执行过程中的错误和异常。我们还可以编写单元测试来验证程序的正确性和健壮性。 最后,我们可以使用Java的打包工具将程序打包成可执行的应用程序或部署到服务器。我们可以使用Java的发布工具将程序分发给用户,让他们可以在其设备上运行我们的程序。 总之,构建Java程序需要掌握Java语言和编程技巧,具备逻辑思维和程序设计能力。这需要不断学习和实践,以实现高效、健壮和可维护的程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值