[dsu] codeforces 375D. Tree and Queries

本文介绍了一种利用树链剖分思想优化子树查询的方法,通过保留重儿子信息减少重复计算,将复杂度从O(n^2logn)降低到O(nlognlogn)。适用于树上的一类子树查询问题。

题意:
给出一棵树,1是根节点,n个节点,每个节点有一种颜色。
有m次询问,每次询问给出v k,求以v节点为根的子树中有多少种数量至少为k的颜色,一种颜色的数量就是该颜色的节点的数量。
题解:
离线,回答以v为根的询问时,如果暴力把整棵子树的颜色存进树状数组,复杂度是 O(n2logn)
但是子树信息可以保留到父节点继续使用,如果要保留子树信息的话,容易发现处理两棵子树时,子树间会相互影响,所以先不保留地处理较小的子树,最后保留地处理最大子树。
用树链剖分的思想来说,我们应该保留重儿子的信息,可以证明这样复杂度是 O(nlognlogn)
这种方法对于一类子树上的查询问题有时候还是比较好用的,但是我还不知道中文名叫什么,这个文章讲的十分清楚了,例题也十分精髓。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
typedef pair<int,int> pii;
vector<int>G[N];
int val[N];
int tree[N];
void update(int p, int v){
    for(p = N-p-1; p < N; p += p&-p) tree[p] += v;
}
int getsum(int p){
    int res = 0;
    for(p = N-p-1; p; p -= p&-p) res += tree[p];
    return res;
}
int sz[N];
void predfs(int rt, int f){
    sz[rt] = 1;
    for(auto& v : G[rt]){
        if(v != f) predfs(v, rt), sz[rt] += sz[v];
    }
}
vector<pii>query[N];
bool bg[N];
int ans[N], col[N];
void add(int rt, int f, int x){
    update(col[val[rt]], -1);
    col[val[rt]] += x;
    update(col[val[rt]], 1);
    for(auto& v : G[rt]){
        if(v != f && !bg[v]) add(v, rt, x);
    }
}
void dfs(int rt, int f, int kp){
    int mx = -1, u = -1;
    for(auto& v : G[rt]){
        if(v != f && sz[v] > mx) mx = sz[v], u = v;
    }
    if(u != -1) bg[u] = 1;
    for(auto& v : G[rt]){
        if(v != f && !bg[v]) dfs(v, rt, 0);
    }
    if(u != -1) dfs(u, rt, 1);
    add(rt, f, 1);
    for(auto& x : query[rt]) ans[x.first] = getsum(x.second);
    if(u != -1) bg[u] = 0;
    if(!kp) add(rt, f, -1);
}
int main(){
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++i) scanf("%d", val+i);
    for(int i = 1; i < n; ++i){
        int a, b;
        scanf("%d%d", &a, &b);
        G[a].push_back(b);
        G[b].push_back(a);
    }
    predfs(1, -1);
    for(int i = 1; i <= m; ++i){
        int v, k;
        scanf("%d%d", &v, &k);
        query[v].push_back(pii(i, k));
    }
    dfs(1, -1, 0);
    for(int i = 1; i <= m; ++i) printf("%d\n", ans[i]);
}
### 并查集 (DSU) 的基本概念 并查集(Disjoint Set Union, DSU),也称为联合-查找数据结构,是一种树形的数据结构,用于处理一些不相交集合的合并及查询问题。这种数据结构支持两种操作:`find` 和 `merge`。 ### Find 方法的作用及实现 `find` 函数的主要目的是找到某个节点所属集合的代表元。为了优化性能,在执行查找的同时会进行路径压缩,使得该集合中的每一个成员都直接连接到根节点上。这不仅提高了后续查找的速度,而且简化了内部结构[^1]。 ```cpp int find(int x) { if (parent[x] != x) { // 如果当前结点不是自己的父节点,则递归寻找其祖先 parent[x] = find(parent[x]); // 路径压缩:将x直接连向它的祖宗节点 } return parent[x]; // 返回最终找到的那个祖先作为整个集合的代表者 } ``` ### Merge 方法的作用及实现 `merge` 或称作 `union` 操作负责将两个不同的集合合并成一个新的集合。具体做法是通过调用两次 `find` 来获取各自集合的代表元素,然后让其中一个成为另一个的父亲即可完成合并过程。通常还会加入按秩合并策略以进一步提升效率。 ```cpp void merge(int x, int y) { int rootX = find(x); int rootY = find(y); if (rootX == rootY) return; // 已经属于同一个集合则无需再做任何事情 // 将较小的一棵树接到较大的一棵下面,保持平衡性 if (rank[rootX] < rank[rootY]) swap(rootX, rootY); parent[rootY] = rootX; if (rank[rootX] == rank[rootY]) ++rank[rootX]; } ``` ### 嵌套 Find 的情况分析 当存在多层嵌套调用 `find` 函数时,由于每次都会触发路径压缩机制,因此即使最初形成了一条很长链表式的子树,在经历一次完整的遍历之后也会变得非常扁平化。这意味着对于任意给定的输入序列而言,实际运行时间接近于常量级别 O(α(n)),其中 α 表示反阿克曼函数,增长极其缓慢几乎可视为常数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值