【模板】点分治

点分治

点分治有两种统计答案的方案
方案1:分别处理子树
在处理完一颗子树的后, 再将节点统一加进备选答案中, 可以避免同颗子树上的两点相互干扰
洛谷P3806 【模板】点分治1

#include<bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10;
const int LIMIT = 1e8 + 10;
const int inf = 0x3f3f3f3f;
int m;

bitset<LIMIT> exist;
int head[N], ver[N << 1], cost[N << 1], nxt[N << 1], tot;
int cnt, temp[N];//temp表示该子树的状态, 在下一子树时会更新进exist
void add(int u, int v, int w){
    ver[++tot] = v;
    cost[tot] = w;
    nxt[tot] = head[u];
    head[u] = tot;
}

int ques[N], ans[N];
int del[N], sz[N], d[N];
int tmp, st, root;
void get_gravity(int n, int u, int fa){
    sz[u] = 1;
    int max_part = 0;
    for(int i = head[u]; i; i = nxt[i]){
        int v = ver[i];
        if(v == fa || del[v]) continue;
        get_gravity(n, v, u);
        sz[u] += sz[v];
        max_part = max(max_part, sz[v]);        
    }
    max_part = max(max_part, n - sz[u]);
    if(max_part < tmp){
        tmp = max_part;
        root = u;
    }
}

inline void getAns(int x){
    for(int i = 1; i <= m; i++) {
        int k = ques[i];
        if(k - x >= 0 && exist[k - x]) ans[i] = 1;
    }
}
void getDis(int u, int fa){
    for(int i = head[u]; i; i = nxt[i]){
        int v = ver[i], w = cost[i];
        if(v == fa || del[v]) continue;
        d[v] = d[u] + w;
        getAns(d[v]);
        temp[++cnt] = d[v];
        getDis(v, u);
    }
}
void calc(int u) {
    cnt = 0, st = 0;
    d[u] = 0;
    exist[d[u]] = 1;//exist[0] = 1
    for(int i = head[u]; i; i = nxt[i]){
        int v = ver[i], w = cost[i];
        if(del[v]) continue;
        d[v] = d[u] + w;
        getAns(d[v]);
        temp[++cnt] = d[v];
        //分别处理每颗子树 
        getDis(v, u);
        for(int j = st; j <= cnt; j++) exist[temp[j]] = 1; 
        st = cnt + 1;
    }
    // 分治完后, 还原数组
    for(int i = 1; i <= cnt; i++) exist[temp[i]] = 0;
}
void vertex_partition(int u, int n){//点分治主要函数 
    tmp = inf;
    get_gravity(n, u, 0);//重心 
	u = root;

    calc(u);
    
    del[root] = 1;
    for(int i = head[root]; i; i = nxt[i]){
    	int v = ver[i];
    	if(del[v]) continue;
    	vertex_partition(v, sz[v]);
	}
}

void solve(){
    int n;
    scanf("%d%d", &n, &m);
    
    for(int i = 1; i <= n - 1; i++){
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        add(u, v, w);
        add(v, u, w);
    }
    for(int i = 1; i <= m; i++){
        int k;
        scanf("%d", &k);
        ques[i] = k;
    }

    vertex_partition(1, n);
    for(int i = 1; i <= m; i++){
    	if(ans[i]) printf("AYE\n");
    	else printf("NAY\n");
	}
}
int main() {
    int t = 1;
    while(t--)solve();

    return 0;
}

用vector开树状数组, 可以实现类似 Σ s i z e = n \Sigma size = n Σsize=n的操作, 其中 s i z e size size为同一层的各个点树状数组的大小

定义:点分治子树:点分治的每一层, 选取了重心后, 该重心的任一棵子树

应用 :指针扫描数组的方法中, 对于点分治过程中的某一层,用 r o o t root root​​​表示重心,对于经过 r o o t root root​的任意两个点 u , v u, v u,v​我们有公式 f ( u , v ) f(u, v) f(u,v)​[ u , v u, v u,v​不属于同一棵子树] = f ( u , v ) − f ( u , v ) [ u , v = f(u, v)- f(u, v)[u, v =f(u,v)f(u,v)[u,v​属于同一颗子树 ] ] ]​ ,其中 f ( u , v ) [ u , v f(u, v)[u, v f(u,v)[u,v​属于用一颗子树 ] ] ]​​​即要求我们统计一棵点分治子树的贡献

定理:n个节点的树, 点分治子树总数等于n - 1.

证明:我们可以把每个点分治子树和点分治子树的重心建立一个双射,则除了第一个点, 其余点都与一个点分治子树一一对应。

定理应用:

HDU4918中, 我们要对每个节点建立树状数组, 运用该定理, 我们知道树状数组的总个数为2N。 (区别于树状数组的总长度2NlogN)

给一棵树, 实现两种操作
1:修改一个节点的权值
2:询问, 到u点距离不超过d的所有节点, 权值和是多少

#include<bits/stdc++.h>
#define lowbit(x) ((x) & (-x))
using namespace std;

const int N = 1e5 + 10;
const int inf = 0x3f3f3f3f;
struct GraphEdge {
    int v, nxt;
};
struct NodeEdge {//链式前向星用
    int root_idx, son_of_root_idx, nxt, dis_to_root;
};
struct BinaryIndexedTree {//树状数组
    int n;
    vector<int> tree;
    void init(int len) {
        n = len;
        tree = vector<int>(len + 1);
    }
    void add(int p, int x) {
        for (int i = p; i <= n; i += lowbit(i))
            tree[i] += x;
    }
    int ask(int p) {
        int ans = 0;
        if (p > n) p = n;
        for (int i = p; i > 0; i -= lowbit(i))
            ans += tree[i];
        return ans;
    }
};
int w[N], del[N];
int Ghead[N], Gtot, Nhead[N], Ntot;
GraphEdge Gedge[N << 1];
NodeEdge Nedge[N << 5];
BinaryIndexedTree tree[N << 1];

int ncnt;
void add_graph(int u, int v) {
    Gedge[++Gtot] = { v, Ghead[u] };
    Ghead[u] = Gtot;
}
void add_node(int u, int root, int son_of_root, int dis) {
    Nedge[++Ntot] = { root, son_of_root, Nhead[u], dis };
    Nhead[u] = Ntot;
}

int root, temp;
int sz[N], dis[N];
void get_gravity(int u, int fa, int n) {
    sz[u] = 1;
    int max_part = 0;
    for (int i = Ghead[u]; i; i = Gedge[i].nxt) {
        int v = Gedge[i].v;
        if (v == fa || del[v])continue;
        get_gravity(v, u, n);
        sz[u] += sz[v];
        max_part = max(max_part, sz[v]);
    }
    max_part = max(max_part, n - sz[u]);
    if (max_part < temp) {
        temp = max_part;
        root = u;
    }
}
void get_size(int u, int fa) {//提根后树型改变, 需要重新更新size
    sz[u] = 1;
    for (int i = Ghead[u]; i; i = Gedge[i].nxt) {
        int v = Gedge[i].v;
        if (v == fa || del[v])continue;
        get_size(v, u);
        sz[u] += sz[v];
    }
}
void dfs(int u, int fa, const int root_idx, const int subroot_idx) {
    for (int i = Ghead[u]; i; i = Gedge[i].nxt) {
        int v = Gedge[i].v;
        if (v == fa || del[v]) continue;
        dis[v] = dis[u] + 1;
        tree[root_idx].add(dis[v] + 1, w[v]);
        tree[subroot_idx].add(dis[v] + 1, w[v]);
        add_node(v, root_idx, ncnt, dis[v]);
        dfs(v, u, root_idx, subroot_idx);
    }
}
void calc(int u) {
    tree[++ncnt].init(sz[u] + 1);
    int root_idx = ncnt;
    dis[u] = 0;
    tree[root_idx].add(dis[u] + 1, w[u]);//树状数组只能统计所有1-n的区间, 故对所有值加1的偏移量
    add_node(u, root_idx, 0, dis[u]); // 小于等于d的所有点也包括自己
    for (int i = Ghead[u]; i; i = Gedge[i].nxt) {
        int v = Gedge[i].v;
        if (del[v]) continue;
        tree[++ncnt].init(sz[v] + 1);//Sigma (sz[v]) = sz[u], 保证这点才不会炸内存, +1与树状数组的平移有关
        dis[v] = dis[u] + 1;
        tree[root_idx].add(dis[v] + 1, w[v]);
        tree[ncnt].add(dis[v] + 1, w[v]);
        add_node(v, root_idx, ncnt, dis[v]);
        dfs(v, u, root_idx, ncnt);
    }
}
void vertex_divide(int n, int u) {
    temp = inf;
    get_gravity(u, 0, n);
    u = root;
    get_size(u, 0);

    calc(u);

    del[u] = 1;
    for (int i = Ghead[u]; i; i = Gedge[i].nxt) {
        int v = Gedge[i].v;
        if (del[v]) continue;
        vertex_divide(sz[v], v);
    }
}
void init(int n) {
    Ntot = Gtot = ncnt = 0;
    memset(Ghead, 0, 4 * (n + 1));
    memset(Nhead, 0, 4 * (n + 1));
    memset(del, 0, 4 * (n + 1));
}
void solve(int n, int q) {
    init(n);
    for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        scanf("%d%d", &u, &v);
        add_graph(u, v);
        add_graph(v, u);
    }
    vertex_divide(n, 1);
    while (q--) {
        char op[5];
        int u, x;
        scanf("%s%d%d", op, &u, &x);
        if (op[0] == '?') {
            int ans1 = 0, ans2 = 0;
            for (int i = Nhead[u]; i; i = Nedge[i].nxt) {
                int root_idx = Nedge[i].root_idx;
                int son_of_root_idx = Nedge[i].son_of_root_idx;
                int dis = Nedge[i].dis_to_root;
                ans1 += tree[root_idx].ask(x - dis + 1);
                ans2 += tree[son_of_root_idx].ask(x - dis + 1);
            }
            printf("%d\n", ans1 - ans2);
        }
        else {
            for (int i = Nhead[u]; i; i = Nedge[i].nxt) {
                int root_idx = Nedge[i].root_idx;
                int son_of_root_idx = Nedge[i].son_of_root_idx;
                int dis = Nedge[i].dis_to_root;
                tree[root_idx].add(dis + 1, x - w[u]);
                tree[son_of_root_idx].add(dis + 1, x - w[u]);
            }
            w[u] = x;
        }
    }
}
int main() {
    int n, q;
    while (~scanf("%d%d", &n, &q)) solve(n, q);

    return 0;
}

检查点分治的时间复杂度:
蓝书上曾有一句话, 在点分治类的做法中, 时间复杂度很容易假, 而对于一些假的复杂度,菊花套链很容易卡掉。
Prime Distance On Tree 为例
题意:简化后,是给一棵边权为1的树, 问树上两点间距离为质数的点有几对
做法:点分治+fft
在进行fft时, 我们发现fft的次数最高可以高达n次(构造一个菊花图即可,相对的, fft数组的长度会很小)那么, 我们该如何计算该算法的时间复杂度呢
S S S为当前正在处理的树, S 1 , S 2 , . . . S_1, S_2, ... S1,S2,...为它的子树, 若当前正在处理第 i i i棵子树, 则进行fft的两个数组, 长度分别为 m a x ( d [ S 1 ] , d [ S 2 ] , . . . d [ S i − 1 ] ) max(d[S_1], d[S_2], ... d[S_{i-1}]) max(d[S1],d[S2],...d[Si1]) d [ S i ] d[S_i] d[Si],(其中 d [ S i ] d[S_i] d[Si]表示 S i S_i Si这课子树中最深的节点的深度),也就是说, 若 d [ S 1 ] d[S_1] d[S1]特别大(如 d [ S 1 ] = n 2 d[S_1] = \frac{n}{2} d[S1]=2n), 则接下来的每一次fft都需要 O ( n 2 l o g n 2 ) O(\frac{n}{2}log\frac{n}{2}) O(2nlog2n)的时间, 结合我们前面所说的fft最多可进行n次, 忽略常数后, 时间复杂度高达 O ( n 2 l o g n ) O(n^2logn) O(n2logn), 这个上界不能让我们满意。 那么该怎么做才能确保时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)
我们考虑对子树按照 d d d排序,则有 m a x ( d [ S 1 ] , d [ S 2 ] , . . . , d [ S i ] ) = d [ S i ] max(d[S_1], d[S_2], ... ,d[S_i]) = d[S_i] max(d[S1],d[S2],...,d[Si])=d[Si]这样进行一次fft的时间复杂度为 O ( d [ S i − 1 ] + d [ S i ] l o g ( d [ S i − 1 ] + d [ S i ] ) ) ≤ O ( 2 ∗ d [ S i ] l o g ( 2 ∗ d [ S i ] ) ) O(d[S_{i-1}] + d[S_i]log(d[S_{i-1}] + d[S_i])) \le O(2*d[S_i]log(2*d[S_i])) O(d[Si1]+d[Si]log(d[Si1]+d[Si]))O(2d[Si]log(2d[Si])),
d [ S i ] ≤ s z [ S i ] d[S_i]\le sz[S_i] d[Si]sz[Si]忽略常数后, 对所有子树求和。 则时间复杂度为 Σ i O ( s z [ S i ] l o g ( s z [ S i ] ) = Σ O ( s z [ S ] l o g ( s z [ S ] ) \Sigma_i O(sz[S_i]log(sz[S_i]) = \Sigma O(sz[S]log(sz[S]) ΣiO(sz[Si]log(sz[Si])=ΣO(sz[S]log(sz[S]), 对于每一层的所有子树, 时间复杂度总和为 O ( n l o g n ) O(nlogn) O(nlogn), 点分治不超过 l o g log log 层, 总时间复杂度为 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)

所以,在如下代码中, 我们特别注意这一段话

vector<pair<int, int>> prior;
for (int i = head[u]; i; i = nxt[i]) { // 提前进行一次dfs, 确定遍历优先级
    int v = ver[i];
    if (del[v]) continue;
    maxD = 0;
    getDis(v, u, 0); // 第一次调用, 根据dis确定优先级,不改变其他内容, add定为0
    prior.push_back({maxD, v});
}
sort(prior.begin(), prior.end()); // !!! 根据maxD改变遍历顺序, 才能保证时间复杂度 

hack数据
构造两条长脸

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;
const int INF = 0x3f3f3f3f;
const int MOD = 998244353;
const double PI = acos(-1.0);
typedef double db;
typedef long double ld;
int head[N], ver[N << 1], nxt[N << 1], tot;
int ans[N];
void add(int u, int v){
    ver[++tot] = v;
    nxt[tot] = head[u];
    head[u] = tot;
}
namespace FFT{
const int MAX_LEN = 2e5 + 10;
const int LOG = __lg(MAX_LEN + 1);
struct cp
{
    db r;db v;
    friend cp operator +(cp a,cp b)
    {
        return (cp){a.r+b.r,a.v+b.v};
    }
    friend cp operator -(cp a,cp b)
    {
        return (cp){a.r-b.r,a.v-b.v};
    }
    friend cp operator *(cp a,cp b)
    {
        return (cp){a.r*b.r-a.v*b.v,a.r*b.v+a.v*b.r};
    }
    void operator /=(const int& b){r/=b;v/=b;}
    
};
int rev[LOG + 1][N];
cp rt[2][LOG + 1][N];
void init() // main函数中注意调用init
{
    for(int d=1;d<=LOG;d++)
        for(int i=1;i<(1<<d);i++)
            rev[d][i]=(rev[d][i>>1]>>1)|((i&1)<<(d-1));
    for(int o=0;o<=1;o++) // 预处理可以提高精度 
        for(int i=1,len=1;i<=LOG;i++,len<<=1)
            for(int j=0;j<(1<<i);j++)
                rt[o][i][j]=(cp){cos(j*PI/len),sin(j*PI/len)*(o?-1:1)};
}
void fft(cp* a,int len,int d,int o) // len必须为2的整数倍, 且len == (1 << d). o = 0为dft, 1为idft
{
    for(int i=0;i<len;i++)
        if(i<rev[d][i])swap(a[i],a[rev[d][i]]);
    int i;int c2=1;
    for(int i=1;i<len;i<<=1,c2++)
    {
        for(int j=0;j<len;j+=(i<<1))
        {
            int c=0;
            for(int k=j;k<i+j;k++,c++)
            {
                cp a1=a[k];cp a2=a[k+i]*rt[o][c2][c];
                a[k]=a1+a2;a[k+i]=a1-a2;
            }
        }
    }
     
    if(o)for(int i=0;i<len;i++)a[i]/=len;
}
};
FFT::cp x[N], y[N];
namespace VertexDivide{
    int sz[N], d[N], cntThis[N], cntSum[N];
    bool del[N];
    int tmp, root, maxD;
    void getGravity(int u, int fa, int n) {
        sz[u] = 1;
        int maxPart = 0;
        for (int i = head[u]; i; i = nxt[i]) {
            int v = ver[i];
            if (v == fa || del[v]) continue;
            getGravity(v, u, n);
            sz[u] += sz[v];
            maxPart = max(maxPart, sz[v]);
        }
        maxPart = max(maxPart, n - maxPart);
        if (maxPart < tmp) {
            tmp = maxPart;
            root = u;
        }
    }
    void getDis(int u, int fa, int add) {
        d[u] = d[fa] + 1;
        cntThis[d[u]] += add;
        maxD = max(maxD, d[u]);
        for (int i = head[u]; i; i = nxt[i]) {
            int v = ver[i];
            if (v == fa || del[v]) continue;
            getDis(v, u, add);
        }
    }
    void calc(int u) {
        d[u] = 0; cntSum[0]++;
        vector<pair<int, int>> prior;
        for (int i = head[u]; i; i = nxt[i]) { // 提前进行一次dfs, 确定遍历优先级
            int v = ver[i];
            if (del[v]) continue;
            maxD = 0;
            getDis(v, u, 0); // 第一次调用, 根据dis确定优先级,不改变其他内容, add定为0
            prior.push_back({maxD, v});
        }
        sort(prior.begin(), prior.end()); // !!! 根据maxD改变遍历顺序, 才能保证时间复杂度 
        maxD = 0;
        for (int i = 0; i < prior.size(); i++) {
            int maxD = prior[i].first, v = prior[i].second;
            if (del[v]) continue;
            getDis(v, u, 1); // 第二次调用, add定为1
            int len = 1, d = 0;
            while (len <= 2 * maxD) len <<= 1, d++;
            for (int j = 0; j < len; j++) {
                x[j].r = cntThis[j];
                x[j].v = 0;
                y[j].r = cntSum[j];
                y[j].v = 0;
            }
            
            FFT::fft(x, len, d, 0);
            FFT::fft(y, len, d, 0);
            for (int j = 0; j < len; j++) 
                x[j] = x[j] * y[j];
            FFT::fft(x, len, d, 1);
            for (int j = 0; j < len; j++) {
                ans[j] += (ll)(x[j].r + 0.5);
            }
            // 统计答案, 清空当前子树
            for (int j = 0; j <= maxD; j++) {
                cntSum[j] += cntThis[j];
                cntThis[j] = 0;
            }
        }
        // 清空整棵树
        for (int j = 0; j <= maxD; j++) {
            cntSum[j] = 0;
        }
    }
    void divide(int u, int n) {
        tmp = INF;
        getGravity(u, 0, n);
        u = root;
        calc(u);

        del[u] = 1;
        for (int i = head[u]; i; i = nxt[i]) {
            int v = ver[i];
            if (del[v]) continue;
            divide(v, sz[v]);
        }
    }
}

void solve() {
    int n;
    cin >> n;
    FFT::init();

    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        add(u, v);
        add(v, u);
    }
    VertexDivide::divide(1, n);
    vector<int> vis(n + 1), prime;
    vis[1] = 1;
    for (int i = 2; i <= n; i++) {
        if (!vis[i]) prime.push_back(i);
        for (auto p : prime) {
            if (i * p > n) break;
            vis[i * p] = 1;
            if (i % p == 0) break;
        }
    }
    int cnt = 0, cntAll = 0;
    for (int i = 0; i <= n; i++) {
        cntAll += ans[i];
        if (!vis[i]) cnt += ans[i];
    }
    cout << (1.0 * cnt / cntAll) << endl;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    int t = 1;
    // cin >> t;
    while (t--) solve();
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值