点分治(奶龙写的)
点分治及其应用
思想
先说一下点分治的基本思想:选择树上一个点作为分治中心,为了保证复杂度,选择的点有一些特殊的要求。
接下来,把原问题分解成几个相同的子问题,进行递归解决。
一般地,我们假设当前根节点为 \(rt\),所以我们要统计的路径必然满足以下二者之一:
-
经过 \(rt\)。
-
不经过 \(rt\),就是在 \(rt\) 的子树上。
树的重心
上面说到,为了保证时间复杂度,我们选择的点有一些特殊要求。一般地,我们选择的点为树的重心。因为这样剩下的子树的最大大小不超过整棵树大小的一半,所以这样递归层数为 \(O(log n)\) 级别的。
为了帮助读者更好的理解上文,这里放一下查找树的重心的代码:
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
算法:点分治,树的重心。
这道例题比较简单,但是实测如果每次都跑一次点分治的话时间不能接受,所以我们考虑把询问离线下来,然后做一次点分治处理所有操作。
下面说一下这道题的做法:
考虑路径是否经过根 \(rt\),不经过的我们递归处理,这里考虑经过的怎么做。
比较显然地,如果在一个子树中距离 \(rt\) 长为 \(l\) 的链,并且距离 \(rt\) 长为 \(k-l\) 的链在其他子树中出现,那么长为 \(k\) 的链一定存在。
并且,两者的出现顺序对于我们的答案没有影响,于是我们便可以一棵一棵子树维护,具体分为下面几步:
-
\(dfs\) 处理当前子树上每个点与根 \(rt\) 的距离。
-
在 \(dfs\) 时,记录哪些长度的链出现过。
-
与之前已经统计过的子树的数据相结合,并且进行更新。
-
清空当前子树的边的信息,这里为了保证时间复杂度,不能使用 \(memset\)。
-
最后递归进入子树进行点分治。
有一些小细节和注释,放在了下面的代码中:
#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;
}
点分治上面的三个函数比较模板化,最多只需要微调,建议理解性背诵,而 \(solve\) 函数需要根据题目来推,相对不需要过多记忆。
Tree
算法:点分治,树的重心,双指针。
根据上面的模板题,这道题的思路还是比较好得出的。
我们考虑一个事情,暴力维护根 \(rt\) 到其他点的距离,但这样时间复杂度过高,需要优化。
我们还是考虑和上一题一样,把不经过根 \(rt\) 的路径进行递归处理,剩下的路径拆成两半计算,这两半在 \(rt\) 不同的子树内。
那我们如何判断两条路径在不在同一棵子树内呢?可以记录每个点 \(x\) 所属的子树 \(b_x\),特别地,\(b_{rt}=rt\)。
接着我们记录所有点到根 \(rt\) 的距离,并且从小到大排序,得到数组 \(subt\)。
然后我们采用双指针 \(l,r\) 进行扫描,由于已经排序,所以我们这样写是正确的。
但是,我们会发现当前的 \([l+1,r]\) 区间内的路径有可能与 \(subt_l\) 有相同的根 \(x\),所以我们还需要记录 \([l+1,r]\) 中以 \(x\) 为根的点的数量。
综上,我们最后产生的贡献为 \(r-l-cnt_{subt_l}\),其他部分与上面的模板题基本一致,具体实现可看下面的代码。
#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;
}
聪聪可可
算法:点分治,数学。
放在前面:这个题可以用树形 \(dp\) 来写,但是在这里不再赘述。
这里对路径的分类方式仍然很套路,和上面的分类方式是相同的。
考虑这两种链出现什么情况会满足题意:
-
两边链的长度都是 \(3\) 的倍数。
-
一边链的长度模 \(3\) 余 \(1\),一边链的长度模 \(3\) 余 \(2\)。
根据这个,我们可以比较自然的想到统计每棵子树上与根 \(rt\) 距离模 \(3\) 分别余 \(0,1,2\) 的点的个数 \(b_0,b_1,b_2\)。
为了方便计算,我们在设 \(db_0,db_1,db_2\) 为前面的子树上与根 \(rt\) 距离模 \(3\) 分别余 \(0,1,2\) 的点的个数之和。
所以我们的贡献为 \(b_0\times db_0+b_1\times db_2+b_2\times db_1\).
最后我们在十分套路地把 \(b\) 合并入 \(db\),然后递归子树进行点分治。
需要注意的是,点对是有序的,故我们统计的答案需要变成原来的 \(2\) 倍。同样,形如 \((a,a)\) 的点对也符合要求,故答案要再加上点的个数。
下面给出这道题的代码:
#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;
}
树上游戏
算法:点分治。
困难题。这道题的询问比较显然地引导我们使用点分治。所以我们考虑怎么使用。
首先我们把路径划分成两种,经过根的与不经过根的,我们只考虑经过根的,另一种通过递归解决。
经过根的路径又可以分成两种,以根为一个端点的与不以根为端点的。
我们先考虑以根为端点的路径,如果一个颜色在这条路径上第一次出现,那么我们考虑这条路径的可能条数一定是 \(siz_u\),这里指 \(u\) 的子树大小。这个还是比较好理解的,就不再过多解释了。
接下来考虑另一种情况,比较套路地,我们把\(u\rightarrow v\) 的路径拆成两半,分别为 \(rt\rightarrow u,rt\rightarrow v\),这里的 \(rt\) 为树根。
下文中的 \(son\) 为 \(u\) 所在子树的根。
先考虑 \(rt\rightarrow u\) 的部分的贡献,可以发现,如果一个路径在 \(rt\rightarrow u\) 上第一次出现,那么他会造成贡献。这时我们看 \(v\) 的可能点数,显然为 \(siz_{rt}-siz_{son}\),于是贡献为上文提到的那个东西乘上路径上的颜色个数。
再考虑 \(rt\rightarrow v\) 的部分的贡献,当然这里要先把 \(son\) 所在子树的东西清掉。具体地,如果发现一个颜色在 \(rt\rightarrow u\) 上第一次出现,把他对根的贡献清掉。
同样,如果在 \(rt\rightarrow v\) 的路径上的某一个颜色在 \(rt\rightarrow u\) 上出现过,那么这个颜色一定在计算 \(rt\rightarrow u\) 的贡献是被算过了,所以我们也要把这个颜色的贡献清掉。
上述过程均可以使用 \(dfs\) 完成,在统计完贡献后记得还原子树 \(son\) 的数据。
最后放一下这道题的代码:
#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;
}

浙公网安备 33010602011771号