网络小说(novel)
题目描述:
给定一个 \(n\) 个点,\(m\) 条边的无向图,点有点权,边有边权,定义两个点之间的距离为两点间的所有路径中的边权的最大值的最小值。
现在问你,所有点权差不小于 \(d\) 的点对之间的距离和。
\(n \leq 2 \times 10^{5} \quad m \leq 5 \times 10^{5}\)
解题思路:
首先注意到,题目定义的距离为 两点间的所有路径中的边权的最大值的最小值 那么这就启发我们想到 kruskal 重构树。
有一个朴素想法,先建出重构树,然后枚举合法点对,求他们在树上的 LCA,时间复杂度为 \(O(n^2logn)\)。
考虑优化,注意到其实我们不需要枚举点对,直接计算每个重构树上的非叶子节点的贡献即可。
因为 kruskal 重构树是一个二叉树,所以只需要计算其中一个子树对另外一个子树的贡献,那么这就可以想到树上启发式合并。
具体做法就是先建出重构树,对于一个非叶子节点,我们先递归小子树,然后递归大子树,将两个子树内部的贡献算好,同时在我们递归大子树时,将其的叶子结点放入树状数组。然后再次递归小子树,这时候我们计算的是两个子树间有多少合法点对,这我们直接在树状数组上查找即可。
时间复杂度为 \(O(nlog^2n)\)。
重构树的具体实现不再赘述,树上启发式合并再多嘴几句,我们在递归时打个标记表示这个子树要不要保留,假设这一整个子树都要保留那么我们将小子树的叶子结点也加入树状数组,否则我们删除大子树的所有叶子结点。
代码实现:
#include<bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int N = 2e6 + 10;
int n, m, d, tot, cnt;
int fa[N], c[N], a[N], tmp[N];
int ls[N], rs[N], l[N], r[N], siz[N];
struct Edge{
int x, y, w;
bool operator < (const Edge A){
return w < A.w;
}
}e[N];
struct BIT{//树状数组
int t[N];
int lowbit(int x){
return x & (-x);
}
void update(int x, int val){
for(int i = x; i <= cnt; i += lowbit(i))
t[i] += val;
}
int query(int x){
int res = 0;
for(int i = x; i; i -= lowbit(i))
res += t[i];
return res;
}
}tr;
void lsh(){
sort(tmp + 1, tmp + 1 + n);
cnt = unique(tmp + 1, tmp + 1 + n) - tmp - 1;
for(int i = 1; i <= n; i++) c[i] = lower_bound(tmp + 1, tmp + 1 + cnt, c[i]) - tmp;
}
int find(int x){//并查集
if(x == fa[x]) return x;
return fa[x] = find(fa[x]);
}
void merge(int x, int y){
fa[y] = x;
siz[x] += siz[y];
}
void kruskal(){//重构树
tot = n;
for(int i = 1; i <= m; i++){
int x = e[i].x, y = e[i].y, w = e[i].w;
if(find(x) == find(y)) continue;
a[++tot] = w;
x = find(x), y = find(y);
if(siz[x] > siz[y]) ls[tot] = y, rs[tot] = x;
else ls[tot] = x, rs[tot] = y;
merge(tot, x), merge(tot, y);
}
}
int dfs1(int x){
int res = 0;
if(x <= n) return tr.query(cnt) - tr.query(r[c[x]] - 1 + (!d)) + tr.query(l[c[x]]);
res = dfs1(ls[x]) + dfs1(rs[x]);
return res;
}
void update(int x, int val){
if(x <= n){
tr.update(c[x], val);
return;
}
update(ls[x], val), update(rs[x], val);
}
__int128 ans;//出题人Milmon的毒瘤数据
void dfs(int x, bool del){//这里和题解不太一样,我这里记的是是否删除这个子树,但本质是一样的
if(x <= n){
if(!del) tr.update(c[x], 1);
return;
}
dfs(ls[x], 1);//先递归小子树
dfs(rs[x], 0);//再递归大子树
ans += dfs1(ls[x]) * a[x];//这里再次递归小子树计算贡献
if(del) update(rs[x], -1);//更新树状数组
else update(ls[x], 1);
}
char ch[20];
int top;
void write() {
if(!ans) ch[++top] = 0;
while(ans != 0) ch[++top] = ans % 10, ans /= 10;
while(top) putchar(ch[top--] + '0');
putchar('\n');
return;
}
signed main(){
freopen("novel.in", "r", stdin);
freopen("novel.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m >> d;
for(int i = 1; i <= n; i++) cin >> c[i], tmp[i] = c[i];
lsh();//因为要使用的是权值树状数组所以需要离散化
for(int lst = 0, i = 1; i <= cnt; i++){//求出符合答案的前缀
while(lst + 1 <= i && tmp[i] - tmp[lst + 1] >= d) lst++;
l[i] = lst;
}
for(int lst = cnt + 1, i = cnt; i >= 1; i--){//求出符合答案的后缀
while(lst - 1 >= i && tmp[lst - 1] - tmp[i] >= d) lst--;
r[i] = lst;
}
for(int i = 1; i <= 2 * n; i++) fa[i] = i, siz[i] = 1;
for(int i = 1; i <= m; i++)
cin >> e[i].x >> e[i].y >> e[i].w;
sort(e + 1, e + 1 + m);
kruskal();
dfs(find(1), 1);
write();
return 0;
}
/*
4 5 2
6 4 5 2
1 2 8
2 3 7
2 4 8
1 3 2
1 4 1
*/