跳转至

树论-静态点分治

点分治,是一种针对树上简单路径统计问题的算法。

例题

P3806 【模板】点分治 1

题目描述

给定一棵有 \(n\) 个点的树,询问树上距离为 \(k\) 的点对是否存在。

做法

对于整棵树的一个根 \(rt\) 来说,路径可以分为两种,经过 \(rt\) 的路径和不经过 \(rt\) 的路径。

其中设路径的端点为 \((u,v)\)

对于第一种路径来说,路径长度就为 \(dis(u,rt)+dis(v,rt)\) ,所以可以预处理出来 \(dis[u]\) 数组,表示 \(u\)\(rt\) 的距离。

对于第二种路径来说,它不经过 \(rt\) ,那么它就包含在 \(rt\) 的某棵子树内,那么就可以找到这颗子树的根,再对它求一次,第一种路径。

也就是说,把整棵树分成许多子树,对每棵子树分别求解第一种路径,随后继续将子树分下去…

所以时间复杂度会与分子树时的递归层数 \(T\) 有关,且每一层的所有递归过程合计对每个点处理一次,所以时间复杂度为 \(O(T * n)\)

如果这棵树退化成一条链时, \(T=n\) ,复杂度为 \(O(n^2)\)

为了让 \(T\) 减小,所以将子树的重心作为子树的根进行处理,此时 \(T=\log n\) ,总复杂度为 \(O(n\log n)\)

这就是点分治!

点分治的时候,不会出现重复的点对。


那么对于本题来说,询问可以离线处理。

具体的细节还是看代码吧!

code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+10;
int read(){//快读
    int r=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-')f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        r=(r<<3)+(r<<1)+(ch-'0');
        ch=getchar();
    }
    return r*f;
}
int n,m;
struct node {
    int t,w;
    node(int a,int b) {
        t=a,w=b;
    }
};
vector<node>v[N];
int si[N],v1[N],msi[N],rt,cnt;
//si[s]:以s为根的子树大小   v1:这个点在点分治过程中是否遍历   msi[s]:表示删除结点s后产生的子树中,最大的那棵的大小
//rt:重心 cnt:这棵树大小
void dfs(int s,int fa) {//找重心
    si[s]=1;
    msi[s]=0;
    for(int i=0; i<v[s].size(); i++) {
        int y=v[s][i].t;
        if(y==fa||v1[y])continue;
        dfs(y,s);
        si[s]+=si[y];
        msi[s]=max(msi[s],si[y]);
    }
    msi[s]=max(msi[s],cnt-si[s]);
    if(msi[s]<msi[rt])rt=s;
}
int q[N],d[N];//d:到根的距离
int ma[10000010],maa[N],tot;//存之前已经处理完的子树的信息:距离
vector<int>vv;
bool fl[N];//fl:是否有这个距离的路径
void dfs1(int s,int fa) {//处理这颗子树的所有点到根距离
    vv.push_back(d[s]);//这颗子树的所有距离
    for(int i=0;i<v[s].size();i++){
        int y=v[s][i].t;
        int w=v[s][i].w;
        if(y==fa||v1[y])continue;
        d[y]=d[s]+w;
        dfs1(y,s);
    }
}
inline void solve(int s,int fa){
    tot=0;
    for(int i=0;i<v[s].size();i++){
        int y=v[s][i].t;
        int w=v[s][i].w;
        if(y==fa||v1[y])continue;
        vv.clear();
        d[y]=w;
        dfs1(y,s);//对于每棵子树进行处理距离
        for(int j=0;j<vv.size();j++){
            for(int k=1;k<=m;k++){
                if(q[k]>=vv[j]){
                    fl[k]|=ma[q[k]-vv[j]];
                }
            }
        }
        for(int j=0;j<vv.size();j++){
            if(vv[j]<=10000000)maa[++tot]=vv[j],ma[vv[j]]=1;
        }
    }
    for(int i=1;i<=tot;i++){
        ma[maa[i]]=0;
        maa[i]=0;
    }
    //最后要清空
}
void dianfenzhi(int s,int fa) {//点分治
    v1[s]=1;//这个点已被遍历
    ma[0]=1;//距离为0肯定是存在的
    solve(s,fa);//处理
    for(int i=0; i<v[s].size(); i++) {
        int y=v[s][i].t;
        if(y==fa||v1[y])continue;
        cnt=si[y];
        rt=0;
        msi[rt]=1e15;
        dfs(y,0);//找重心
        dianfenzhi(rt,0);//继续点分治
    }
}
signed main() {
    n=read(),m=read();
    for(int i=1; i<n; i++) {
        int x,y,z;
        x=read(),y=read(),z=read();
        v[x].push_back(node(y,z));
        v[y].push_back(node(x,z));
    }
    for(int i=1; i<=m; i++)q[i]=read();//离线
    msi[rt]=1e15;
    cnt=n;
    dfs(1,0);//先找重心
    dianfenzhi(rt,0);//点分治
    for(int i=1; i<=m; i++) {
        if(fl[i])cout<<"AYE\n";
        else cout<<"NAY\n";
    }
    return 0;
}

题单

P2634 [国家集训队] 聪聪可可

板题,对于 \(\%3\) 后的数进行储存,然后算贡献就行了。

P4149 [IOI2011] Race

板题,再统计一下边的数量就行了。

P4178 Tree

搞棵树状数组就行了。

P3714 [BJOI2017] 树的难题

发现拼路径的时候不仅需要知道根到点的权值之和,还需要记录边的数量以及与根相连的边的颜色。

首先处理与根相连的边的颜色:考虑排序,将每种颜色依次处理。

然后以边数为下标建立两棵线段树,第一棵线段树记录与现在颜色不同的权值最大值,第二棵线段树记录与现在颜色相同的权值最大值。

最后算一下贡献就行了,复杂度 \(O(n \log^2 n)\)

使用单调队列可以将复杂度降到 \(O(n \log n)\)

未完待续?