点分治笔记

算法思想

点分治作为分治算法,将问题分成了经过重心和未经过重心两部分,点分治经常用于带权树上的路径统计,本质为优化的暴力算法,分治只是解决问题的思路,点分治的时间复杂度是靠重心保证的,整体时间复杂度其实应该为O(hn),h为树高

重心:删除该节点后得到的最大子树的节点数最少

重心将当前树切割成了多个大体相同的子树,使得子问题的规模大致相同,保证了时间复杂度

重心相关性质参考:算法学习笔记(72): 树的重心

求树的重心,进行一次DFS,找到删除该节点后最大子树的最小的节点,f[u]表示删除u后最大子树的大小,size[u]表示以u为根的子树的节点数,S表示整棵子树的节点数,先统计u的所有子树中最大子树的节点数f[u],然后与S-size[u]比较,取max

如图
在这里插入图片描述

代码实现

void getroot(int u,int fa)//f[0]需要初始化为无穷大
{
	Size[u]=1;//初始化大小,只有自己一个节点
	f[u]=0;//初始化
	for(int i=head[u];i;i=e[i].next)
	{
		int v=e[i].to;
		getroot(v,u);
		Size[u]+=Size[v];//累加子树大小
		f[u]=max(f[u],Size[v]);//获得子树中的最大值
	}
	f[u]=max(f[u],S-Size[u]);//获得删除u之后的最大子树的大小
	if(f[u]<f[root])//重心定义
		root=u;
}

统计各点到重心的距离,用DFS实现

代码实现

void getdep(int u,int fa)
{
	dep[++dep[0]]=d[u];//用dep[0]当计数器
	for(int i=head[u];i;i=e[i].next)
	{
		int v=e[i].to;
		if(!vis[v]&&v!=fa)
		{
			d[v]=d[u]+e[i].w;
			getdep(v,u);
		}
	}
}

对于根为u的子树求出重心,然后统计满足条件的路径

代码实现

void getsum(int u,int dis)//获取以u为根的子树中满足条件的个数
{
	d[u]=dis;
	dep[0]=0;
	sort(dep+1,dep+1+dep[0]);
	int L=1,R=dep[0];sum=0;
	while(L<R){统计的内容}
	return sum;
}

统计满足条件的路径数,每次查找通过当前重心的方案数,之后去掉重心,递归求解子树,需要去重,去掉重复的路径

代码实现

void solve(int u)//这里传入的是整棵树的重心
{
	vis[u]=1;
	ans+=getsum(u,0);
	for(int i=head[u];i;i=e[i].next)
	{
		int v=e[i].to;
		if(!vis[v])
		{
			ans-=getsum(v,e[i].w);//去掉重复的路径
			root=0;
			S=Size[v];
			getroot(v,0);//得到v为根子树的重心
			solve(root);//对重心递归操作
		}
	}
}

关于重复计算路径的问题

在这里插入图片描述
以计算路径长度不超过4为例,由图可知,在计算以1为根的时候,已经算过了1-3-7的路径,但是在以3为根的子树中3-7又是符合条件的,而树中任意两节点间的路径都是不重复的,因此需要去重

训练

Luogu P3525

题目大意: 一棵n个节点的树,行动中心S从1->N。从S出发前往任意一个未标记到的点(沿树上两点的唯一路径走),标记该节点,然后返回S。相邻两次行动所经过的道路不允许有重复,最后一次标记后不需要返回,求路程总和的最小值。第i行输出行动中心为i时的答案,如果不可能则输出-1

思路:这题可以用树形DP换根什么的,但是我不会(惨笑),看了洛谷的第一篇题解后,根据题解的大致意思写出了代码,这里具体阐述一下这个题的思路

由题意,相邻两次不能有公共边,意思为以S为根的数每次取的点不能来自S的同一棵子树,进而可以推出子树规模不能超过n/2,那么就有如下几种情况

  1. 根节点为重心,最大子树规模小于n/2
  2. 根节点为重心,最大子树规模为n/2
  3. 根节点不为重心

对第一种情况,在规模不超过n/2时,用最大子树的节点去匹配其他子树的节点,如果匹配完了就用次大子树来匹配其他未匹配的子树,以此类推,因为求路程总和最小值,那么最后一次标记就选择深度最大的子树即可

对第二种情况,规模等于n/2时(n为偶数),其余子树的总大小为n/2-1,也就是说,用最大子树去匹配其余子树必定剩余一个节点,为了使得该节点被访问,并求得路径和最小,最后一次标记只能走最大子树中的最长链的最后一个节点

对第三种情况,必然有最大子树规模大于n/2,当以该子树匹配完其他子树节点后,该最大子树必然剩余至少两个节点,而如果标记这两个节点,必然会有重复路径,不符合题设条件

代码

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
int n,head[maxn],s[maxn],cnt,root1,root2,f[maxn]= {maxn*10};
int d[maxn],dsum,maxx;
bool vis[maxn];
struct node {
    int to,nt;
} e[maxn*2];
void Add(int from,int to) {
    e[++cnt].to=to;
    e[cnt].nt=head[from];
    head[from]=cnt;
}
void getroot1(int u,int fa) {
    s[u]=1;
    f[u]=0;
    for(int i=head[u]; i; i=e[i].nt) {
        int v=e[i].to;
        if(v==fa)continue;
        getroot1(v,u);
        s[u]+=s[v];
        f[u]=max(f[u],s[v]);
    }
    f[u]=max(f[u],n-s[u]);
    if(f[u]<f[root1])
        root1=u;
}
void getdep(int u,int fa) {
    vis[u]=1;
    for(int i=head[u]; i; i=e[i].nt) {
        int v=e[i].to;
        if(!vis[v]&&v!=fa) {
            d[v]=0;
            d[v]=d[u]+1;
            getdep(v,u);
        }
    }
    vis[u]=0;
}
void dfs(int u,int&ans,int fa) {
    vis[u]=1;
    for(int i=head[u]; i; i=e[i].nt) {
        int v=e[i].to;
        if(!vis[v]&&v!=fa) {
            ans=max(ans,d[v]);
            dfs(v,ans,u);
        }
    }
    vis[u]=0;
}
int main() {
    scanf("%d",&n);
    for(int i=1; i<=n-1; i++) {
        int u,v;
        scanf("%d%d",&u,&v);
        Add(u,v);
        Add(v,u);
    }
    getroot1(1,0);//确定第一个重心
    for(int i=head[root1]; i; i=e[i].nt) {//确定第二个重心
        int v=e[i].to;
        if(f[v]==f[root1]) {
            root2=v;
            break;
        }
    }
    for(int i=1; i<=n; i++)
        if(i==root1||i==root2) {//如果是重心
            dsum=0;
            maxx=0;
            memset(vis,0,sizeof(vis));
            memset(d,0,sizeof(d));
            getroot1(i,0);//构造以该重心为基础的s数组
            getdep(i,0);//构造以该重心为基础的d数组
            if(n%2==0&&f[i]==n/2) {//特判是否最大子树大小就为n/2
                for(int j=head[i]; j; j=e[j].nt) {//在最大子树中找最大链
                    int v=e[j].to;
                    if(s[v]==n/2) {//最大深度
                        maxx=max(maxx,d[v]);
                        dfs(v,maxx,i);
                        break;
                    }
                }
            } else
                for(int j=1; j<=n; j++)maxx=max(maxx,d[j]);//最大深度
            for(int j=1; j<=n; j++)//深度和统计
                dsum+=d[j];
            printf("%d\n",dsum*2-maxx);
        } else printf("-1\n");
    return 0;
}
/*
4
1 2
1 3
3 4
*/

POJ1741

题目大意:一棵有n个节点的树,每边都有长度(小于1001),dist(u,v)为节点u和v的最小距离,给定一个整数k,对每对节点(u,v),当且仅当dis(u,v)不超过k时才叫有效,计算给定的树种有多少对节点有效

思路:查询树中有多少对节点距离不超过k,相当于查询树上两点间距不超过k的路径有多少条,以树的重心root划分点,则树上两点u、v的路径分为两种:①经过root;②不经过root,只需对每棵树求解满足第一种路径第二种路径可以继续点分治

代码

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdlib>
using namespace std;
typedef struct node {
    int next,to,w;
} node;
node e[212121];
int n,u,v,l,k,head[12121],cnt,f[12121]={99999},Size[12121],root,ans,dep[12121],d[12121],S;
//f[0]tmd需要初始化
bool vis[12121];
void Add(int from,int to,int value) {//链式前向星
    e[++cnt].to=to;
    e[cnt].next=head[from];
    e[cnt].w=value;
    head[from]=cnt;
}
void getdep(int u,int fa) {//获得每个点到达重心的距离,getdep是用来调用的
    dep[++dep[0]]=d[u];//dep[0]用来记录数量,这里只是存值,无需保证顺序
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(v!=fa&&!vis[v]) {//非父节点&已访问
            d[v]=d[u]+e[i].w;
            getdep(v,u);
        }
    }
}
void getroot(int u,int fa) {//获取每棵单独树的重心
    Size[u]=1;
    f[u]=0;//删除u后最大子树的大小c
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(v!=fa&&!vis[v]) {
            getroot(v,u);
            Size[u]+=Size[v];//累和u的子树总结点和
            f[u]=max(f[u],Size[v]);//获得去掉u后u的最大子树
        }
    }
    f[u]=max(f[u],S-Size[u]);//S-Size[u]为删去u及其子树后剩下的节点数
    //在max之前,f[u]为u中子树最大值
    if(f[u]<f[root])//使删去u之后的最大子树尽量小,f[0]需要初始化
        root=u;
}
int getsum(int u,int dis) {//获取以u为根的子树满足条件个数
    d[u]=dis;
    dep[0]=0;
    getdep(u,0);//获取各点到重心距离
    sort(dep+1,dep+1+dep[0]);//升序
    int L=1,R=dep[0],sum=0;
    while(L<R)//类似尺取法取得值
        if(dep[L]+dep[R]<=k)
            sum+=R-L,L++;
        else
            R--;
    return sum;
}
void solve(int u) {
    vis[u]=1;//标记该点
    ans+=getsum(u,0);//获取以该点为根的子树满足条件个数
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!vis[v]) {//如果没被访问过
            ans-=getsum(v,e[i].w);//去重
            root=0;//初始化重心
            getroot(v,0);//获得重心
            S=Size[v];//更新子树的整个大小
            solve(root);
        }
    }
}
int main() {
    ios::sync_with_stdio(0),cin.tie(0);
    while(cin >>n>>k&&n&&k) {
        for(int i=0; i<n-1; i++) {
            cin >>u>>v>>l;
            Add(u,v,l);
            Add(v,u,l);
        }
        S=n;
        solve(1);
        cout <<ans<<endl;
        cnt=S=ans=0;
        memset(head,0,sizeof(head));
        memset(vis,0,sizeof(vis));
        memset(Size,0,sizeof(Size));
    }
    return 0;
}

POJ2114

题目大意:给出一棵树,每条边有权值,确定树上是否存在一对村庄编号a,b,使得ab间路径距离恰好是给定值x

思路:大体思路与上题一样,不过在对dep数组扫描的时候是判断两端和是否为x

代码

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef struct node {
    int w,next,to;
} node;
node e[121212];
int N,cnt,head[121212],M,f[121212]= {0x3f3f3f3f},d[121212],dep[121212],Size[121212],S,k,ans,root;
bool vis[121212];
void Add(int from,int to,int value) {
    e[++cnt].to=to;
    e[cnt].next=head[from];
    e[cnt].w=value;
    head[from]=cnt;
}
void getroot(int u,int fa) {//获得重心
    Size[u]=1;
    f[u]=0;
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!vis[v]&&v!=fa) {
            getroot(v,u);
            Size[u]+=Size[v];
            f[u]=max(f[u],Size[v]);
        }
    }
    f[u]=max(f[u],S-Size[u]);
    if(f[u]<f[root])
        root=u;
}
void getdep(int u,int fa) {
    dep[++dep[0]]=d[u];//获得距离数组
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(v!=fa&&!vis[v]&&d[u]+e[i].w<=k) {//剪枝
            d[v]=d[u]+e[i].w;//更新
            getdep(v,u);
        }
    }
}
int getsum(int u,int dis) {
    d[u]=dis;
    dep[0]=0;
    getdep(u,0);
    sort(dep+1,dep+1+dep[0]);
    int L=1,R=dep[0],sum=0;
    while(L<R)
        if(dep[L]+dep[R]<k)//和小于,L右移
            L++;
        else if(dep[L]+dep[R]>k)
            R--;
        else {
            if(dep[L]==dep[R]) {//首尾相等,代表中间相等,全组合
                sum+=(R-L+1)*(R-L)/2;
                break;
            }
            int op=L,ed=R;
            while(dep[op]==dep[L])//找到首个不等值
                op++;
            while(dep[ed]==dep[R])
                ed--;
            sum+=(op-L)*(R-ed);
            L=op,R=ed;//缩小区间,继续求值
        }
    return sum;
}
void solve(int u) {
    vis[u]=1;
    ans+=getsum(u,0);
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!vis[v]) {
            ans-=getsum(v,e[i].w);//去重
            root=0;
            S=Size[v];
            getroot(v,u);//获得重心,此时构造出来的Size数组是相对于v的,不是相对于根节点的,root为子树重心
            solve(root);
        }
    }
    vis[u]=0;//归零
}
int main() {
    ios::sync_with_stdio(0),cin.tie(0);
    while(cin >>N&&N) {
        for(int i=1; i<=N; i++) {//存图
            int id,value;
            while(1) {
                cin >>id;
                if(id==0)
                    break;
                cin >>value;
                Add(i,id,value);
                Add(id,i,value);
            }
        }
        while(cin >>k&&k) {
            ans=root=S=0;//初始化
            //memset(vis,0,sizeof(vis));
            getroot(1,0);//找出root
            getroot(root,0);//构造root的Size数组
            solve(root);//计算满足条件值
            if(ans>0)
                cout <<"AYE"<<endl;
            else
                cout <<"NAY"<<endl;
        }
        memset(head,0,sizeof(head));//清空
        cnt=0;
        cout <<"."<<endl;
    }
    return 0;
}

HDU4812

题目大意:给出一棵树,树上每个节点都有一个值,询问是否存在一个链使得链上所有整数乘积等于给定值k,输出字典序最小的方案(节点对)

思路:同样使用点分治,主要处理的是经过重心的情况,遍历子树,将root到节点u路径上的节点乘积存储到d[u],如果一条链(u,v)满足d[v]×d[u]/val[root]=k,则u与v构成一组解(计算d[v],d[u]时val多乘了一次),可得d[v]=k×val[root]/d[u],由于题目中数据量很大,采用乘法逆元,设inv[d[u]]为d[u]逆元,公式变为d[v]=k×val[root]×inv[d[u]]

求重心,DFS,记录root到u节点值乘积,构造积到映射的编号
对root的每棵子树,求解子树v中每个节点到root路径上节点的积,查询与该节点配对的另一节点inv[x]×val[root]×k%P,判断该积是否映射有节点。子树v中所有节点在查询完毕后把积x映射到另一节点,保证不在一棵子树内查询,因为这些节点还没有映射
查询完毕后清空映射,递归求解子树

关于逆元

具体意义与推导略,给出结论,i关于1模P的乘法的逆元为inv[i]=(-P/i)×inv[P%i]%P

关于代码中的solve部分
在这里插入图片描述

getdep(v,u);//构造以u为重心的树各节点到重心的乘积积
	for(int j=1; j<=top; j++)
		query(dep[j],id[j]);//直接查找逆元是否存在
	for(int j=1; j<=top; j++)
		if(!mp[dep[j]]||mp[dep[j]]>id[j])//更新索引,由字典序决定
			mp[dep[j]]=id[j];

这个部分其实想了一点时间才理解,首先,每次使用这段代码的时候,v都是变化的,代码的目的其实是形成mp这个索引,以上图为例,第一次操作左边红色的子树时,第一次的结果必定是没有的,因为此时的索引还未建立,但是当操作到第二次,操作的对象为右边蓝色的子树,索引已经形成,只需要更新与判断即可,也就是说对于一个新的子树,每次将前面多棵子树操作结果的合并供当前子树查询,并插入该子树

代码

#include <bits/stdc++.h>
#pragma comment(linker,"/STACK:102400000,102400000")
using namespace std;
typedef long long ll;
const ll P=1e6+3,maxn=1e5+10;
ll inv[P+5],mp[P+5],ans1=0x3f3f3f3f,ans2=0x3f3f3f3f;//inv存储逆元,mp将乘积映射到节点
ll val[maxn],d[maxn],dep[maxn];//节点的值,节点到树根的乘积,乘积序列
int cnt,n,k,top,head[maxn],id[maxn],root,f[maxn]= {0x3f3f3f3f},S,Size[maxn]; //id为节点序列
bool vis[maxn];
typedef struct node {
    int next,to;
} node;
node e[maxn*2];
void Add(int from,int to) {
    e[++cnt].to=to;
    e[cnt].next=head[from];
    head[from]=cnt;
}
void getinv() {//求1~P-1的逆元
    inv[1]=1;
    for(int i=2; i<P; i++)
        inv[i]=((-P/i)*inv[P%i]%P+P)%P;
}
void getroot(int u,int fa) {//得重心
    f[u]=0;
    Size[u]=1;
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!vis[v]&&v!=fa) {
            getroot(v,u);//对子树操作
            Size[u]+=Size[v];//获得以u为根节点的子树大小
            f[u]=max(f[u],Size[v]);//获得子树最大值
        }
    }
    f[u]=max(f[u],S-Size[u]);
    if(f[u]<f[root])
        root=u;
}
void getdep(int u,int fa) {
    dep[++top]=d[u];//这里用的是top,不是dep[0]
    id[top]=u;//dep和id一同构造了乘积(dep)与编号(id)的联系
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(v!=fa&&!vis[v]) {
            d[v]=(d[u]*val[v])%P;//计算当前点到树根的乘积
            getdep(v,u);
        }
    }
}
void query(int x,int id) {
    x=inv[x]*val[root]*k%P;//求另一个点的乘积
    int y=mp[x];//获得编号
    if(y==0)
        return;
    if(y>id)//根据字典序排序
        swap(y,id);
    if(y<ans1||(y==ans1&&id<ans2))//字典序更小更改答案
        ans1=y,ans2=id;
}
void solve(int u) {
    vis[u]=1;
    mp[val[u]]=u;//建立索引
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!vis[v]) {
            top=0;//求以v为根的子树,
            d[v]=val[v]*val[u]%P;//累积
            getdep(v,u);//构造以u为重心的树各节点到重心的乘积积
            for(int j=1; j<=top; j++)
                query(dep[j],id[j]);//直接查找逆元是否存在
            for(int j=1; j<=top; j++)
                if(!mp[dep[j]]||mp[dep[j]]>id[j])//更新索引,由字典序决定
                    mp[dep[j]]=id[j];
        }
    }
    mp[val[u]]=0;
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!vis[v]) {
            top=0;
            d[v]=(val[u]*val[v])%P;
            getdep(v,u);
            for(int j=1; j<=top; j++)
                mp[dep[j]]=0;//清空使用过的mp
        }
    }
    for(int i=head[u]; i; i=e[i].next) {//进行下一层
        int v=e[i].to;
        if(!vis[v]) {
            root=0;
            S=Size[v];
            getroot(v,0);
            getroot(root,0);
            solve(root);
        }
    }
    vis[u]=0;
}
int main() {
    ios::sync_with_stdio(0),cin.tie(0);
    getinv();//构造逆元
    while(cin >>n>>k) {
        for(int i=1; i<=n; i++)//录入节点值
            cin >>val[i];
        for(int i=1; i<n; i++) {//录入图
            int x,y;
            cin >>x>>y;
            Add(x,y);
            Add(y,x);
        }
        getroot(1,0);//获得根节点
        getroot(root,0);//获得Size数组
        solve(root);
        if(ans1!=0x3f3f3f3f&&ans2!=0x3f3f3f3f)
            cout <<ans1<<" "<<ans2<<endl;
        else
            cout <<"No solution\n";
        cnt=root=0;//root需要初始化
        memset(head,0,sizeof(head));
        ans1=ans2=0x3f3f3f3f;
    }
    return 0;
}

HDU4918

题目大意:一棵树,节点编号1~n,每个点默认权值 w i w_i wi,两种操作:①! v x,将节点v权值修改为x,②?v d查询v到距离不超过d的所有节点的权值和。 节点u和v的距离为它们之间最短路径上的边数

思路:点更新+查询,这里就需要使用树状数组/线段树这样的数据结构了,对点分治,树的重心一共有 log ⁡ N \log N logN层,第一层为整棵树重心,第二层为第一层重心子树的重心,每次都至少分成两个大小差不多的子树,共 log ⁡ N \log N logN层,每个节点最多属于 log ⁡ N \log N logN个重心

本题查询到v距离不超过d的所有节点的权值之和,如果把这些节点权值升序排序,问题便成为前缀和问题,为每个重心创造两个树状数组,第一个维护节点v到当前重心的距离,第二个维护节点v到上一层重心的距离,统计时去掉重复统计的部分,第二个为去重作用,如图,黄色节点到蓝色重心的距离为黄色节点到上一层重心的距离减去蓝色重心到上一层节点的距离

在这里插入图片描述

代码

/*
HDU4918
1.求树的重心root
2.从root出发统计每个节点到root的距离d1,将节点权值插入第一个树状数组,若重心的层数大于1,求节点到上一层重新的距离d2
将该节点的权值插入第二个树状数组
3.求解root的每棵子树,这些子树的重心为第二层,递归
4.查询到节点x不超过y的节点权值之和,首先统计与当前重心≤y-dis[cur[k]][x]的节点权值之和,若当前重心层次大于1,需要去重
(与上一层重心的距离≤y-dis[cur[k]-1][x]的节点权值之和),向上统计一层,递归
5.更新节点值为y,更新第一个树状数组,将x到当前重心距离为dis[cur[k]][x]的节点值更新为y,若当前重心大于1,更新上一层,递归
*/
#include <bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
const int maxn=1e5+10;
int n,q,val[maxn],cnt,head[maxn],S,Size[maxn],f[maxn]= {0x3f3f3f3f},root;
int dis[121][maxn],tot,id[maxn],F[maxn],cur[maxn],ans;//
bool vis[maxn];
struct BIT {//树状数组
    int n,*C;
    void init(int T) {//初始化
        n=T;
        if(C)
            delete C;
        C=new int[T+1];
        for(int i=0; i<=n; i++)
            C[i]=0;
    }
    int que(int x) {//查询前缀和
        int sum=0;
        x++;
        for(; x>0; x-=-x&x)
            sum+=C[x];
        return sum;
    }
    void add(int x,int y) {//点更新
        x++;
        for(; x<=n; x+=x&-x)
            C[x]+=y;
    }
    ~BIT() {
        delete C;
    }
} C[4*maxn][2];//只需要两个树状数组,当前重心和上一重心
struct node {
    int to,next;
} e[maxn*2];
void Add(int from,int to) {//链式前向星
    e[++cnt].next=head[from];
    e[cnt].to=to;
    head[from]=cnt;
}
void getroot(int u,int fa) {//获取重心
    f[u]=0;
    Size[u]=1;
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(v!=fa&&!vis[v]) {
            getroot(v,u);
            Size[u]+=Size[v];
            f[u]=max(f[u],Size[v]);
        }
    }
    f[u]=max(f[u],S-Size[u]);
    if(f[u]<f[root])
        root=u;
}
void DFS(int dep,int idx,int u,int d,int fa) {//重心层次,在树状数组中的位置,当前节点,到根节点的距离,父节点
    C[idx][0].add(dis[dep][u]=d,val[u]);//将节点权值插入树状数组
    if(dep>1)//若重心层次大于1,求该节点到上一层重心距离
        C[idx][1].add(dis[dep-1][u],val[u]);//上一层重心
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(v!=fa&&!vis[v])
            DFS(dep,idx,v,d+1,u);
    }
}
void solve(int u,int fa) {//solve的参数始终为重心
    vis[u]=1;//标记
    F[id[u]=++tot]=id[fa];//id[u]为此时u编号(在树状数组中),F记录u的父节点(即上一层重心)的编号
    cur[id[u]]=cur[id[fa]]+1;//cur记录当前节点层数
    C[tot][0].init(S+1);//初始化树状数组
    C[tot][1].init(S+1);
    DFS(cur[id[u]],id[u],u,0,0);//从root出发统计每个节点到重心距离
    int tmp=S;
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(vis[v])
            continue;
        root=0;
        S=Size[u]>Size[v]?Size[v]:tmp-Size[u];//对不同节点求得S
        getroot(v,u);//获得重心
        getroot(root,u);//构造Size
        solve(root,u);
    }
    vis[u]=0;
}
void Query(int k,int x,int y) {
    int d=max(-1,min(C[k][0].n-1,y-dis[cur[k]][x]));
    ans+=C[k][0].que(d);
    if(F[k]) {
        d=max(-1,min(C[k][1].n-1,y-dis[cur[k]-1][x]));
        ans-=C[k][1].que(d);//去重
        Query(F[k],x,y);
    }
}
void Update(int k,int x,int y) {
    C[k][0].add(dis[cur[k]][x],y);
    if(F[k]) {//如果父节点(即上一层重心存在)
        C[k][1].add(dis[cur[k]-1][x],y);//更新父节点
        Update(F[k],x,y);//更新父节点
    }
}
int main() {
    ios::sync_with_stdio(0),cin.tie(0);
    while(cin >>n>>q) {
        for(int i=1; i<=n; i++)
            cin >>val[i];
        for(int i=1; i<n; i++) {//存图
            int a,b;
            cin >>a>>b;
            Add(a,b);
            Add(b,a);
        }
        S=n;
        getroot(1,0);
        getroot(root,0);//获得整棵树的重心
        solve(root,0);//构造树状数组
        while(q--) {
            char ch;
            int a,b;
            cin >>ch>>a>>b;
            if(ch=='!') {
                root=0;
                //S=n;
                //getroot(a,0)
                Update(id[a],a,b-val[a]);//必须传入更改的差
                val[a]=b;//防二次更改
            } else {
                ans=root=0;
                //S=n;
                //getroot(a,0);
                Query(id[a],a,b);//传入的是该节点重心在树状数组上的编号
                cout <<ans<<endl;
            }
        }
        cnt=tot=0;
        memset(head,0,sizeof(head));
        memset(cur,0,sizeof(cur));
        memset(id,0,sizeof(id));
    }
    return 0;
}
/*
9 1
3 1 2 6 4 5 8 2 3
1 2
1 3
1 4
2 5
2 6
3 7
6 8
6 9
? 2 2
*/

总结

点分治大多用来求解树上满足条件的路径条数,满足其复杂度条件的关键是重心的选取与操作,点分治可以与修改、查询类的数据结构在树上一起使用,是一个较难掌握的算法

参考文献

  1. 算法学习笔记(72): 树的重心
  2. P3525 [POI2011]INS-Inspection 题解
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值