点分治模板
点分治模板
点分治:
这里统一地,只讲一下主要思想,各种细节留给参考文章。
点分治的主要是处理树上的路径问题,如果用暴力做法求解会有很高的时间复杂度\(O(n^2)-O(n^3)\)的样子。而如果使用点分治,就可以将复杂度降为\(O(nlog_2n)\)。主要思路是将一棵树分解成若干子树,在分解成子树的子树,递归地求解出部分答案(分),最后在递归回去时合并答案(治)。至于怎么分解树,这里是要找到一个合理的根。使得这个跟的最大子树能够最小,这样的根叫做树的重心。举个栗子:一条链,是从端点处一个一个处理到头快呢,还是从中心往两边处理快?是中心处理快(\(O(log_2n)\)),而不是端点\(O(n)\)。因此我们需要一个找重心的过程。找到了重心就可以递归地处理子树了。这个过程就是分的过程,我们找到了重心后,在以重心为根的树的子树中再一次找重心,一直分下去。治的过程是对每个重心统计答案的过程,这个过程可能需要求出点到重心的距离,也可能用容斥原理去重,而且可能用到二分加速找答案。最后就在返回后得到了结果。
大概思路是这样(我也不太清晰),具体请参看参考文章。
这里先放一道模板题:P3806 【模板】点分治1的代码,代码里面就包含了基本上最基本点分治的内容。
代码:
#include <iostream>
#include <algorithm>
#define INF 0x3f3f3f3f
#define max_n 200005
using namespace std;
int n,m;//n为节点数,m为询问次数
int ask[max_n];//记录询问距离
//链式前向星
int cnt = 0;
int head[max_n];
struct edge
{
int v;
int w;
int nxt;
}e[max_n<<1];
void add(int u,int v,int w)
{
++cnt;
e[cnt].v=v;
e[cnt].nxt=head[u];
head[u]=cnt;
e[cnt].w=w;//多了边上的权值
}
int rt,ms;//rt为重心,ms是最小 最大子树的大小
int t;//t为dis数组计数量
int Size;//当前整棵树的大小
int sz[max_n];//sz[i]表示以i为根的子树大小
int mson[max_n];//mson[i]表示以i的最大子树的大小
int dis[max_n];//到某节点的距离数组
bool visit[max_n];//标记是否分治过
int ans[max_n];//记录答案
//二分用到的结构数组
int tt;
struct asd
{
int x;//大小
int y;//次数
}arr[max_n];
//求重心函数
void get_root(int u,int from)
{
sz[u]=1;mson[u]=0;//开始时以u为根的子树大小为1,u的最大子树为0
for(int i=head[u];i;i=e[i].nxt)//遍历与u相连边
{
int v=e[i].v;
if(visit[v]||v==from) continue;//防止重复遍历
get_root(v,u);
sz[u]+=sz[v];//更新以u为根的子树大小
mson[u]=max(mson[u],sz[v]);//更新u的最大子树
}
if(Size-sz[u]>mson[u]) mson[u]=Size-sz[u];//看是否另一半子树更大
if(ms>mson[u]) ms=mson[u],rt=u;//更新最小的最大子树和重心
}
//求距离函数
void get_dis(int u,int from,int w)
{//t一开始初始化为0
dis[++t] = w;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(visit[v]||v==from) continue;
get_dis(v,u,w+e[i].w);
}
}
//求解答案函数
void conquer(int x,int y,int id)//x表示get_dis的起始点,y表示节点x到目标点的距离,id为+/-1
{
t=0;//记得初始化dis计数量
tt=0;//记得初始化二分用到的结够数组计数量
get_dis(x,0,y);//计算x到目标点的距离,注意这里y可能因为去重,本身传进来就不是0
sort(dis+1,dis+t+1);//从小到大排序,距离相同的都紧挨在一起
dis[0]=-233;//重要
for(int i = 1;i<=t;i++)
{
if(dis[i]!=dis[i-1]) arr[++tt].x=dis[i],arr[tt].y=1;//如果距离和上一个不一样,新开一个
else arr[tt].y++;//否则就在旧的基础上加一
}
for(int i = 1;i<=m;i++)
{
if(ask[i]%2==0)//看有没有到x距离刚好是k/2的点
{
for(int j=1;j<=tt;j++)
{
if(arr[j].x==ask[i]/2)
{
ans[i]+=(arr[j].y-1)*arr[j].y/2*id;
}
}
}
for(int j=1;j<=tt&&arr[j].x<ask[i]/2;j++)//如果还有可以枚举的,并且距离小于k/2的点,继续
{//不用枚举距离大于k/2的
int l=j+1;int r=tt;
while(l<=r)//二分查找距离和为k的点
{
int mid=(l+r)>>1;
if(arr[mid].x+arr[j].x==ask[i])
{
ans[i]+=arr[j].y*arr[mid].y*id;//统计答案
break;
}
if(arr[mid].x+arr[j].x<ask[i])
{
l = mid+1;
}
else
{
r = mid-1;
}
}
}
}
}
//分函数
void divide(int u,int ssize)
{
visit[u]=true;//当前节点已分治
conquer(u,0,1);//计算以u为重心的所有组合
for(int i = head[u];i;i=e[i].nxt)
{
int v=e[i].v;
if(visit[v]) continue;//已分治过的不用再分治
conquer(v,e[i].w,-1);//所有组合减去不合法的组合,注意第二个参数距离已经加上了u到z的距离
ms=INF;rt=0;//每一次求重心都要初始化这两个值
Size=sz[v]<sz[u]?sz[v]:ssize-sz[u];/*由于dfs的起点不是重心原因,最终算出来的不是真正重心的子树大小,这样做能以重心为参考系,算出以重心为根的子树大小,具体解释见参考文章*/
get_root(v,u);//求出子树的重心
divide(v,Size);//分治子树
}
}
int main()
{
cin >> n >> m;
for(int i = 1;i<n;i++)
{
int u,v,w;
cin >> u >> v >> w;
add(u,v,w);
add(v,u,w);
}
for(int i = 1;i<=m;i++)
{
cin >> ask[i];
}
rt=0;ms=INF;
get_root(1,0);
divide(rt,n);
for(int i = 1;i<=m;i++)
{
if(ans[i]>0)
{
cout << "AYE" << endl;
}
else
{
cout << "NAY" << endl;
}
}
return 0;
}
参考文章:
Hypoc_,树上点分治详解【入门向】,https://blog.csdn.net/a_forever_dream/article/details/81778649(真心讲的细啊讲的好)
守望,点分治略解,https://www.luogu.org/blog/user9012/dian-fen-zhi-lve-xie(点分治的大致思想和细节)
gigo_64,点分治入门,https://blog.csdn.net/qq_42835815/article/details/87067143(与第一个比要简略一点,但思路还是很清晰的)
我大概也只能看得懂入门的文章了吧(/ω\)