点分治
点分治及其应用
思想
先说一下点分治的基本思想:选择树上一个点作为分治中心,为了保证复杂度,选择的点有一些特殊的要求。
接下来,把原问题分解成几个相同的子问题,进行递归解决。
一般地,我们假设当前根节点为
-
经过
。 -
不经过
,就是在 的子树上。
树的重心
上面说到,为了保证时间复杂度,我们选择的点有一些特殊要求。一般地,我们选择的点为树的重心。因为这样剩下的子树的最大大小不超过整棵树大小的一半,所以这样递归层数为
为了帮助读者更好的理解上文,这里放一下查找树的重心的代码:
void get_root(int u,int f){
siz[u]=1;
mx[u]=0;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
get_root(j,u);
siz[u]+=siz[j];
mx[u]=max(mx[u],siz[j]);
}
mx[u]=max(mx[u],sum-siz[u]);
if(mx[u]<mx[rt])rt=u;//根据重心的定义找重心
}
点分治1
算法:点分治,树的重心。
这道例题比较简单,但是实测如果每次都跑一次点分治的话时间不能接受,所以我们考虑把询问离线下来,然后做一次点分治处理所有操作。
下面说一下这道题的做法:
考虑路径是否经过根
比较显然地,如果在一个子树中距离
并且,两者的出现顺序对于我们的答案没有影响,于是我们便可以一棵一棵子树维护,具体分为下面几步:
-
处理当前子树上每个点与根 的距离。 -
在
时,记录哪些长度的链出现过。 -
与之前已经统计过的子树的数据相结合,并且进行更新。
-
清空当前子树的边的信息,这里为了保证时间复杂度,不能使用
。 -
最后递归进入子树进行点分治。
有一些小细节和注释,放在了下面的代码中:
#include<bits/stdc++.h>
#define int long long
#define N 100005
#define M 200005
#define K 10000005
#define inf 2e18
using namespace std;
int n,m,h[N],e[M],w[M],ne[M],idx;
int qs[N],res[N],rt,sum,siz[N],mx[N];
int st[N],tf[K],dis[N],d[N],dcnt,q[N],hh,tt;
/*
第一行基础变量和链式前向星存图
qs 为每个询问,res 为询问结果
sum 子树总大小,rt 当前树根
mx 最大子树大小,st 当前点是否被处理过
tf 是否有某一长度的链,dis 距离根的距离
d 当前子树的链的长度,dcnt 当前子树到根的链的个数
q,hh,tt 队列
*/
void add(int a,int b,int c){
e[idx]=b;w[idx]=c;ne[idx]=h[a];h[a]=idx++;
}
void get_root(int u,int f){
siz[u]=1;
mx[u]=0;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
get_root(j,u);
siz[u]+=siz[j];
mx[u]=max(mx[u],siz[j]);
}
mx[u]=max(mx[u],sum-siz[u]);
if(mx[u]<mx[rt])rt=u;//根据重心的定义找重心
}
void get_dist(int u,int f){
d[++dcnt]=dis[u];//当前的距离存入可能的长度
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
dis[j]=dis[u]+w[i];
get_dist(j,u);
}
}
void solve(int u,int f){
hh=0;tt=-1;
q[++tt]=0;
tf[0]=1;st[u]=1;//当前点已经处理过,且长度为0的链一定存在(即不选)
for(int i=h[u];~i;i=ne[i]){
int v=e[i];
if(v==f||st[v])continue;
dis[v]=w[i];
get_dist(v,u);
for(int k=1;k<=dcnt;k++){//链长度
for(int j=1;j<=m;j++){//询问
if(qs[j]>=d[k]){
res[j]|=tf[qs[j]-d[k]];//如果d[k]和qs[j]-d[k]都出现,就存在
}
}
}
for(int j=1;j<=dcnt;j++){
if(d[j]<10000005){//如果链长度有意义(可能被询问)
q[++tt]=d[j];//就加到队列里
tf[d[j]]=1;//这个长度能被凑出来
}
}
dcnt=0;
}
while(hh<=tt)tf[q[hh++]]=0;//清空之前子树的信息
for(int i=h[u];~i;i=ne[i]){
int j=e[i];//继续找其他子树计算
if(j==f||st[j])continue;
sum=siz[j];rt=0;
mx[rt]=inf;
get_root(j,u);
get_root(rt,-1);
solve(rt,u);
}
}
signed main(){
cin>>n>>m;
memset(h,-1,sizeof h);
for(int i=1;i<n;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);add(b,a,c);
}
for(int i=1;i<=m;i++){
cin>>qs[i];
}
rt=0;mx[rt]=inf;sum=n;
get_root(1,-1);
get_root(rt,-1);
solve(rt,-1);
for(int i=1;i<=m;i++){
if(res[i])cout<<"AYE\n";
else cout<<"NAY\n";
}
return 0;
}
点分治上面的三个函数比较模板化,最多只需要微调,建议理解性背诵,而
Tree
算法:点分治,树的重心,双指针。
根据上面的模板题,这道题的思路还是比较好得出的。
我们考虑一个事情,暴力维护根
我们还是考虑和上一题一样,把不经过根
那我们如何判断两条路径在不在同一棵子树内呢?可以记录每个点
接着我们记录所有点到根
然后我们采用双指针
但是,我们会发现当前的
综上,我们最后产生的贡献为
#include<bits/stdc++.h>
#define int long long
#define N 100005
#define M 200005
#define inf 2e18
using namespace std;
int n,m,h[N],e[M],w[M],ne[M],idx;
int siz[N],mx[N],rt,sum,st[N],dis[N];
int b[N],subt[N],scnt[N],dcnt,res;
bool cmp(int a,int b){
return dis[a]<dis[b];
}
void add(int a,int b,int c){
e[idx]=b;w[idx]=c;ne[idx]=h[a];h[a]=idx++;
}
void get_root(int u,int f){
siz[u]=1;
mx[u]=0;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
get_root(j,u);
siz[u]+=siz[j];
mx[u]=max(mx[u],siz[j]);
}
mx[u]=max(mx[u],sum-siz[u]);
if(mx[u]<mx[rt])rt=u;
}
void get_dist(int u,int f){
if(f!=rt)b[u]=b[f];
else b[u]=u;
subt[++dcnt]=u;scnt[b[u]]++;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
dis[j]=dis[u]+w[i];
get_dist(j,u);
}
}
void solve(int u,int f){
subt[++dcnt]=u;b[u]=u;dis[u]=scnt[u]=0;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
dis[j]=w[i];
scnt[j]=0;
get_dist(j,u);
}
sort(subt+1,subt+dcnt+1,cmp);
int l=1,r=dcnt;
st[u]=1;
while(l<r){
while(dis[subt[l]]+dis[subt[r]]>m){
scnt[b[subt[r]]]--;
r--;
}
res+=r-l-scnt[b[subt[l]]];
l++;
scnt[b[subt[l]]]--;
}
dcnt=0;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
sum=siz[j];
rt=0;
mx[rt]=inf;
get_root(j,u);
get_root(rt,-1);
solve(rt,u);
}
}
signed main(){
cin>>n;
memset(h,-1,sizeof h);
for(int i=1;i<n;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);add(b,a,c);
}
rt=0;
mx[rt]=inf;
sum=n;
get_root(1,-1);
get_root(rt,-1);
cin>>m;
solve(rt,-1);
cout<<res;
return 0;
}
聪聪可可
算法:点分治,数学。
放在前面:这个题可以用树形
这里对路径的分类方式仍然很套路,和上面的分类方式是相同的。
考虑这两种链出现什么情况会满足题意:
-
两边链的长度都是
的倍数。 -
一边链的长度模
余 ,一边链的长度模 余 。
根据这个,我们可以比较自然的想到统计每棵子树上与根
为了方便计算,我们在设
所以我们的贡献为
最后我们在十分套路地把
需要注意的是,点对是有序的,故我们统计的答案需要变成原来的
下面给出这道题的代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
#define M 200005
#define inf 2e18
using namespace std;
int n,h[N],e[M],w[M],ne[M],idx;
int st[N],b[5],db[5],res,rt,sum;
int siz[N],mx[N],dis[N],dcnt;
int gcd(int a,int b){
return b?gcd(b,a%b):a;
}
void add(int a,int b,int c){
e[idx]=b;w[idx]=c;ne[idx]=h[a];h[a]=idx++;
}
void get_root(int u,int f){
siz[u]=1;
mx[u]=0;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
get_root(j,u);
siz[u]+=siz[j];
mx[u]=max(mx[u],siz[j]);
}
mx[u]=max(mx[u],sum-siz[u]);
if(mx[u]<mx[rt])rt=u;
}
void get_dist(int u,int f){
b[dis[u]%3]++;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
dis[j]=dis[u]+w[i];
get_dist(j,u);
}
}
void solve(int u,int f){
st[u]=1;
db[0]=db[1]=db[2]=0;
dis[u]=0;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
b[0]=b[1]=b[2]=0;
dis[j]=w[i];
get_dist(j,u);
res+=b[0]*db[0] +b[1]*db[2]+b[2]*db[1];
db[0]+=b[0];db[1]+=b[1];db[2]+=b[2];
}
res+=db[0];
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
sum=siz[j];
rt=0;
mx[rt]=inf;
get_root(j,u);
get_root(rt,-1);
solve(j,u);
}
}
signed main(){
cin>>n;
memset(h,-1,sizeof h);
for(int i=1;i<n;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);add(b,a,c);
}
sum=n;
rt=0;
mx[rt]=inf;
get_root(1,-1);
get_root(rt,-1);
solve(rt,-1);
res=res*2+n;
int g=gcd(res,n*n);
cout<<res/g<<'/'<<n*n/g;
return 0;
}
树上游戏
算法:点分治。
困难题。这道题的询问比较显然地引导我们使用点分治。所以我们考虑怎么使用。
首先我们把路径划分成两种,经过根的与不经过根的,我们只考虑经过根的,另一种通过递归解决。
经过根的路径又可以分成两种,以根为一个端点的与不以根为端点的。
我们先考虑以根为端点的路径,如果一个颜色在这条路径上第一次出现,那么我们考虑这条路径的可能条数一定是
接下来考虑另一种情况,比较套路地,我们把
下文中的
先考虑
再考虑
同样,如果在
上述过程均可以使用
最后放一下这道题的代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
#define M 200005
#define inf 2e18
using namespace std;
int n,m,h[N],e[M],ne[M],idx;
int rt,sum_siz,col[N],siz[N],mx[N];
int delta,num,sum,cnt[N],col_siz[N],res[N];
bool st[N];
void add(int a,int b){
e[idx]=b;ne[idx]=h[a];h[a]=idx++;
}
void get_root(int u,int f){//经典的板子,找重心代码
siz[u]=1;
mx[u]=0;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
get_root(j,u);
siz[u]+=siz[j];
mx[u]=max(mx[u],siz[j]);
}
mx[u]=max(mx[u],sum_siz-siz[u]);
if(mx[u]<mx[rt])rt=u;
}
void dfs1(int u,int f){//算其中一端为根的贡献
siz[u]=1;
cnt[col[u]]++;//把这个颜色加上
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;//板子
dfs1(j,u);
siz[u]+=siz[j];
}
if(cnt[col[u]]==1){//如果只出现一次,那么这个点会产生贡献
sum+=siz[u];//这个点下面的路径共有siz_u条
col_siz[col[u]]+=siz[u];//这个颜色的贡献也要记得修改
}
cnt[col[u]]--;//回溯时把这个颜色去掉
}
void modify(int u,int f,int val){//修改子树的贡献
cnt[col[u]]++;//同上
if(cnt[col[u]]==1){
sum+=val*siz[u];//看情况是增加还是去掉
col_siz[col[u]]+=val*siz[u];
}
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
modify(j,u,val);
}//这部分代码基本上等同于dfs1,只是看是增加贡献还是减少贡献
cnt[col[u]]--;
}
void dfs2(int u,int f){//这里计算链rt->u和rt->v的贡献
cnt[col[u]]++;//同上
if(cnt[col[u]]==1){
sum-=col_siz[col[u]];//把u所在子树的贡献去掉
num++;
}
res[u]+=sum+num*delta;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
dfs2(j,u);
}
if(cnt[col[u]]==1){
sum+=col_siz[col[u]];//回溯时再加回来
num--;
}
cnt[col[u]]--;
}
void init(int u,int f){
cnt[col[u]]=col_siz[col[u]]=0;//清空
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
init(j,u);
}
}
void solve(int u,int f){
st[u]=1;
dfs1(u,f);
res[u]+=sum;
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
cnt[col[u]]++;
sum-=siz[j];col_siz[col[u]]-=siz[j];
modify(j,u,-1);//去掉u子树的贡献
cnt[col[u]]--;
delta=siz[u]-siz[j];
dfs2(j,u);
cnt[col[u]]++;
sum+=siz[j];col_siz[col[u]]+=siz[j];
modify(j,u,1);//再加回来
cnt[col[u]]--;
}
sum=num=0;
init(u,f);
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f||st[j])continue;
sum_siz=siz[j];
rt=0;mx[rt]=inf;
get_root(j,u);
get_root(rt,-1);
solve(rt,-1);
}
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>col[i];
}
memset(h,-1,sizeof h);
for(int i=1;i<n;i++){
int a,b;
cin>>a>>b;
add(a,b);add(b,a);
}
sum_siz=n;
rt=0;mx[rt]=inf;
get_root(1,-1);
get_root(rt,-1);
solve(rt,-1);
for(int i=1;i<=n;i++){
cout<<res[i]<<'\n';
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】