Analysis
不妨设 g ( x ) g(x) g(x) 为边权为 x x x 的边被且仅被经过 1 次的路径个数。那么答案为 ∑ i = 1 n g ( x ) \sum_{i=1}^ng(x) ∑i=1ng(x)。
接下来分析对于特定 x x x,如何去求得 g ( x ) g(x) g(x)。
以这棵树为例,我们设当前要求 g ( 2 ) g(2) g(2),即通过且仅通过边权为 2 的边一次的路径个数。
我们将所有边权为 2 的边删去,得到下图。
然后发现由于边的断开,这棵树分成了若干个连通块。观察发现,对于每条边权为 2 的边,它对答案的贡献就是它连接的两个连通块节点个数的乘积(确定路径的起点和终点)。
我们沿着这个思想,来考虑如何求得所有的 g ( x ) g(x) g(x)。
Solution 1
第一种解法是并查集分治。
要处理区间 [ l , r ] [l,r] [l,r] 内所有边权的 g ( x ) g(x) g(x) 之和,每次可以从中点将区间分成两半,递归求解左半段时,就将右半段所有边连上;递归求解右半段时,先把右半段所有边断开,再把左半段所有点连上。
这样当我们递归到某个确切的边权 w w w 时,除了 w w w 以外其他边权的所有边都连上了,对每个边权为 w w w 的边求两个端点所在连通块节点个数的乘积。
这一过程可以用栈 + 并查集维护。并查集只能写按秩合并,不写路径压缩,否则无法撤销边。栈内存当前连上的所有边,递归结束回溯时就把栈内多出来的边都撤销。
因为并查集没有路径压缩,所以时间复杂度多了一个 O ( log n ) \operatorname O(\log n) O(logn)。
Code
tourist 大神的现场代码 orz。
/**
* author: tourist
* created: 23.05.2022 18:44:02
**/
#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#include "algo/debug.h"
#else
#define debug(...) 42
#endif
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
vector<vector<pair<int, int>>> g(n);
for (int i = 0; i < n - 1; i++) {
int a, b, c;
cin >> a >> b >> c;
--a; --b; --c;
g[c].emplace_back(a, b);
}
vector<int> p(n);//并查集
iota(p.begin(), p.end(), 0);
vector<int> sz(n, 1);
vector<pair<int, int>> ops;//栈,存储当前所有连起来的边
auto Get = [&](int i