题解:关于带权树中边权组合计算问题
一、题目背景
给定一个由 (n) 个节点组成的树(通过 (n - 1) 条边连接),每条边都有一个权值。需要计算所有可能的边权组合的某种结果(具体为将每条边加入最小生成树时,根据该边连接的两个连通分量的节点数量计算出的结果之和)。
二、解题思路
本题主要利用并查集和贪心算法的思想来解决。核心思路是将树的边按照权值从小到大排序,依次处理每条边,利用并查集维护节点的连通性以及每个连通分量的节点数量。在处理每条边时,根据该边连接的两个连通分量的节点数量计算出相应的结果,并累加到最终答案中。
具体步骤如下:
1. 定义结构体 Edge
来存储边的信息,包括边的两个端点 a
、b
和边权 w
,并重载 <
运算符以便对边按权值进行排序。
2. 定义数组 p[N]
用于并查集存储每个节点的父节点,数组 size[N]
用于记录每个连通分量的节点数量。
3. 读取测试用例的数量 T
,对于每个测试用例:
- 读取节点数量 n
和 (n - 1) 条边的信息(两个端点和边权),将边的信息存储到 e
数组中。
- 对边按照边权从小到大进行排序。
- 初始化并查集,使每个节点的父节点为自身,每个连通分量的节点数量为 (1)。
- 遍历排序后的边数组,对于每条边:
- 查找边的两个端点所在连通分量的根节点 a
和 b
。
- 如果两个端点不在同一个连通分量中:
- 计算该边加入后对结果的贡献,即 (size[a] * size[b] - 1) * (w + 1)
,并累加到结果变量 res
中。
- 合并两个连通分量,更新较大连通分量的节点数量,并将较小连通分量的根节点指向较大连通分量的根节点。
- 输出最终结果 res
。
三、代码逐段分析
(一)头文件和全局变量
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 6010;
int n;
struct Edge
{
int a, b, w;
bool operator< (const Edge &t) const
{
return w < t.w;
}
}e[N];
int p[N], size[N];
- 头文件:
#include <cstring>
:用于字符串操作和内存初始化函数,如memset
。#include <iostream>
:提供 C++ 风格的输入输出流,如cin
和cout
。#include <algorithm>
:包含一些常用的算法函数,如sort
用于排序。using namespace std;
:使用标准命名空间,以便直接使用标准库中的函数和类型。
- 常量定义:
const int N = 6010
:定义节点数量以及边数组的最大长度。
- 变量定义:
int n;
:表示节点的数量。struct Edge
:定义结构体Edge
用于存储边的信息,包括两个端点a
和b
以及边权w
,并重载<
运算符,使得可以按照边权对边进行从小到大的排序。e[N];
:数组e
用于存储所有边的信息。int p[N], size[N];
:p[N]
用于并查集存储每个节点的父节点;size[N]
用于记录每个连通分量的节点数量。
(二)find
函数
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
该函数用于在并查集中查找节点 x
的根节点。如果 p[x]
不等于 x
,说明 x
不是根节点,递归地查找其根节点,并进行路径压缩(将 p[x]
更新为根节点),最后返回根节点。
(三)main
函数
int main()
{
int T;
cin >> T;
while (T -- )
{
cin >> n;
for (int i = 0; i < n - 1; i ++ )
{
int a, b, w;
cin >> a >> b >> w;
e[i] = {a, b, w};
}
sort(e, e + n - 1);
for (int i = 1; i <= n; i ++ ) p[i] = i, size[i] = 1;
int res = 0;
for (int i = 0; i < n - 1; i ++ )
{
int a = find(e[i].a), b = find(e[i].b), w = e[i].w;
if (a != b)
{
res += (size[a] * size[b] - 1) * (w + 1);
size[b] += size[a];
p[a] = b;
}
}
cout << res << endl;
}
return 0;
}
- 输入测试用例数量并循环处理:
int T; cin >> T;
:读取测试用例的数量T
。while (T -- )
:对于每个测试用例进行循环处理。
- 读取树的边信息:
cin >> n;
:读取节点数量n
。- 通过循环
for (int i = 0; i < n - 1; i ++ )
读取 (n - 1) 条边的信息,包括两个端点a
、b
和边权w
,并将边的信息存储到e
数组中(e[i] = {a, b, w};
)。
- 边排序和并查集初始化:
sort(e, e + n - 1);
:对边按照边权从小到大进行排序。- 通过循环
for (int i = 1; i <= n; i ++ )
初始化并查集,使每个节点的父节点为自身(p[i] = i
),每个连通分量的节点数量为 (1)(size[i] = 1
)。
- 计算结果:
int res = 0;
:初始化结果变量res
为 (0)。- 通过循环
for (int i = 0; i < n - 1; i ++ )
遍历排序后的边数组:- 调用
find
函数查找边的两个端点e[i].a
和e[i].b
所在连通分量的根节点a
和b
。 - 如果
a
和b
不相等(即两个端点不在同一个连通分量中):- 计算该边加入后对结果的贡献
(size[a] * size[b] - 1) * (w + 1)
,并累加到res
中。 - 更新较大连通分量的节点数量(
size[b] += size[a];
),并将较小连通分量的根节点a
指向较大连通分量的根节点b
(p[a] = b;
)。
- 计算该边加入后对结果的贡献
- 调用
- 输出结果并结束程序:
cout << res << endl;
:输出最终结果。return 0;
:程序正常结束。
四、时间复杂度和空间复杂度分析
(一)时间复杂度
- 输入边信息:读取 (n - 1) 条边的信息,时间复杂度为 (O(n))。
- 边排序:对 (n - 1) 条边进行排序,时间复杂度为 (O((n - 1) \log (n - 1))),近似为 (O(n \log n))。
- 处理边并计算结果:遍历 (n - 1) 条边,每次处理边时调用
find
函数的时间复杂度为 (O(\alpha(n)))((\alpha(n)) 是阿克曼函数的反函数,在实际应用中可近似看作常数),所以这部分时间复杂度为 (O(n \alpha(n))),近似为 (O(n))。 - 总体时间复杂度为 (O(n + n \log n + n) = O(n \log n)),主要由边排序的时间复杂度决定。
(二)空间复杂度
- 存储边信息:边数组
e[N]
占用空间为 (O(n))。 - 并查集相关数组:数组
p[N]
和size[N]
占用空间均为 (O(n))。 - 总体空间复杂度为 (O(n + n + n) = O(n))。
希望以上题解能帮助你理解这道题的代码逻辑和算法原理。如果还有疑问,请随时提问。