「学习笔记」点分治
引子
点分治, 其实应该叫 "树上点分治".
主要用于解决 "树上路径问题" (我乱起的名字).
比如, 树上是否存在长为 (k) 的路径, 树上长小于 (k) 的路径有多少条等等.
点分治可以概括为 : 分治 + 重心 + 桶 (就目前我做过的几道题来说都是这个套路)
算法过程
我们就直接针对一道题来吧.
换句话说, 就是树上是否存在长度为 (k) 的路径.
首先, 选一个根节点, 然后树上的路径就被分成了两类,
- 经过根节点.
- 不经过根节点.
考虑怎么处理第一类路径.
我们设 (dis[u]) 为点 (u) 到根节点的距离,
建一个桶 (b[i]), 表示是否存在 (dis[u] = i) 的节点.
然后对每一棵子树都 (dfs) 两遍.
第一遍查看桶中是否存在 (dis = k-dis[u]) 的点, 即 (b[k-dis[u]]) 是否为 (1).
第二遍把点放进桶中, 即 (b[dis[u]]=1).
第一类路径就这样处理完了, 时间复杂度为 (O(n)).
而对于第二类路径, 我们可以把它看做经过了另外一个 "根节点" 的第一类路径, 然后就可以递归完成了.
若递归层数为 (T), 总时间复杂度就为 (O(Tn)).
那我们怎么使 (T) 尽量小呢?
答案是------以重心作为根节点.
这还是比较好理解的, 因为重心会将整棵树分成若干个 (size le frac{n}{2}) 的子树, 所以 (T) 就是 (log n) 级别的.
这样, 总复杂度就为 (O(n log n)) 了.
还有一个小问题, 就是我们在处理第一类路径中所要使用的桶, 如果每次递归都要全部清空会浪费很多时间.
因此, 我们在修改桶的时候, 可以用一个队列把修改过的位置记下来, 最后清空的时候只需要把这些位置归零就行了, 这样就保证了每一层递归的修改操作的时间复杂度为 (O(n)).
代码
#include<bits/stdc++.h>
using namespace std;
const int _=1e4+7;
const int __=1e7+7;
const int inf=0x3f3f3f3f;
bool st;
int n,m,maxk=10000000,sz[_],dis[_],rt,minx=inf,q[_],top,qes[100+7];
int lst[_],nxt[2*_],to[2*_],len[2*_],tot;
bool vis[_],b[__],ans[100+7];
bool en;
void add(int x,int y,int w){ nxt[++tot]=lst[x]; to[tot]=y; len[tot]=w; lst[x]=tot; }
void read(){
cin>>n>>m;
int x,y,w;
for(int i=1;i<n;i++){
scanf("%d%d%d",&x,&y,&w);
add(x,y,w);
add(y,x,w);
}
for(int i=1;i<=m;i++) scanf("%d",&qes[i]);
}
void pre(int u,int fa){
sz[u]=1;
for(int i=lst[u];i;i=nxt[i]){
int v=to[i];
if(v==fa||vis[v]) continue;
pre(v,u);
sz[u]+=sz[v];
}
}
void g_rt(int u,int fa,int sum){
int maxn=sum-sz[u];
for(int i=lst[u];i;i=nxt[i]){
int v=to[i];
if(v==fa||vis[v]) continue;
pre(v,u);
maxn=max(maxn,sz[v]);
}
if(maxn<minx){ minx=maxn; rt=u; }
}
void cnt(int u,int fa){
// printf("%d: %d
",u,dis[u]);
for(int i=1;i<=m;i++)
if(qes[i]>=dis[u]&&b[qes[i]-dis[u]]) ans[i]=1;
for(int i=lst[u];i;i=nxt[i]){
int v=to[i];
if(v==fa||vis[v]) continue;
dis[v]=dis[u]+len[i];
cnt(v,u);
}
}
void mrk(int u,int fa){
if(dis[u]>maxk) return;
if(!b[dis[u]]){ q[++top]=dis[u]; b[dis[u]]=1; }
for(int i=lst[u];i;i=nxt[i]){
int v=to[i];
if(v==fa||vis[v]) continue;
mrk(v,u);
}
}
void calc(int u){
for(int i=lst[u];i;i=nxt[i]){
int v=to[i];
if(vis[v]) continue;
dis[v]=len[i];
// printf(" %d: %d
",v,dis[v]);
cnt(v,0); mrk(v,0);
}
for(int i=1;i<=top;i++) b[q[i]]=0;
top=0;
}
void run(int u,int lstrt){
minx=inf;
pre(u,0);
g_rt(u,0,sz[u]);
vis[rt]=1;
// printf("rt: %d
",rt);
u=rt;
for(int i=lst[u];i;i=nxt[i]){
int v=to[i];
if(vis[v]) continue;
run(v,u);
}
calc(u);
vis[u]=0;
}
int main(){
//freopen("template.in","r",stdin);
//freopen("template.out","w",stdout);
read();
b[0]=1;
run(1,0);
for(int i=1;i<=m;i++) puts(ans[i] ?"AYE" :"NAY");
return 0;
}
题目
参考资料
点分治略解 by Dispwnl