简单说一下静态点分治,快过年时候一个下午讲过去,现在都忘得差不多了,赶紧捡起来(背个板子)。
首先,点分治主要解决树的路径点权相关问题,其思想是分治(233)。
我们将树通过(子)重心进行“分块”,不断地进行统计,时间复杂度nlogn大概?
还是先来例题,不然说不明白= =
#POJ1741 树中点对统计
Description
给定一棵N(1<=N<=100000)个结点的带权树,每条边都有一个权值(为正整数,小于等于1001)。定义dis(u,v)为u,v两点间的最短路径长度,路径的长度定义为路径上所有边的权和。再给定一个K(1<=K<=10^9),如果对于不同的两个结点u,v,如果满足dist(u,v)<=K,则称(u,v)为合法点对。求合法点对个数。
Input
输入文件的第一行包含两个整数n和k,接下来n-1行每行都有三个整数u,v,l, 表示结点u和结点v之间有一条长l的边。
Output
输出文件只有一行为合法点对的个数。
Sample Input
5 4
1 2 3
1 3 1
1 4 2
3 5 1
Sample Output
8
Hint
【数据范围】:
对于50%的数据,n<=1000,k<=1000;
对于100%的数据,n<=100000,k<=10^9;
#include<bits/stdc++.h>
using namespace std;
#define Inc(i,L,r) for(register int i=(L);i<=(r);++i)
const int N = 1e5+10;
struct Edge{
int to[N<<1],next[N<<1],w[N<<1];
int cnt,h[N];
inline void add(int x,int y,int z){
next[++cnt]=h[x];
to[cnt]=y;
w[cnt]=z;
h[x]=cnt;
}
}e;
int n,k,G,sum,Minsiz,siz[N];
int ans,dis[N];
bool vst[N];
inline void init(){
scanf("%d%d",&n,&k);
Inc(i,1,n-1){
int x,y,w;scanf("%d%d%d",&x,&y,&w);
e.add(x,y,w),e.add(y,x,w);
}
sum=n;
}
inline void FindG(int x,int fa){//找重心
int Maxsiz=0;
siz[x]=1;
for(int p=e.h[x];p;p=e.next[p])if(e.to[p]^fa&&!vst[e.to[p]]){
FindG(e.to[p],x);
siz[x]+=siz[e.to[p]];
Maxsiz=max(Maxsiz,siz[e.to[p]]);
}
Maxsiz=max(Maxsiz,sum-siz[x]);
if(Maxsiz<Minsiz)G=x,Minsiz=Maxsiz;
}
inline void FindD(int x,int fa,int dist){//记录
dis[++dis[0]]=dist;
for(int p=e.h[x];p;p=e.next[p])if(e.to[p]^fa&&!vst[e.to[p]]){
FindD(e.to[p],x,dist+e.w[p]);
}
}
inline void Cal(int x,int dist,int cmd){//统计
dis[0]=0;
FindD(x,0,dist);
sort(dis+1,dis+dis[0]+1);
int L=1,r=dis[0];
while(L<r){
if(dis[L]+dis[r]<=k)ans+=(r-L)*cmd,++L;
else --r;
}
}
inline void stat(int x){
Minsiz=1<<30;
FindG(x,0);
vst[G]=1;
Cal(G,0,1);
for(int p=e.h[G];p;p=e.next[p])if(!vst[e.to[p]]){
Cal(e.to[p],e.w[p],-1);//容斥
sum=siz[e.to[p]];//注意改sum
stat(e.to[p]);
}
}
int main(){
init();
stat(1);
cout<<ans<<"\n";
return 0;
}
通过代码我们可以感受出点分治的策略,每次以子重心为根,统计子树对根的贡献,然后将子树的贡献“合并起来”,例如我们要统计如下图所示的子树:
假设现在重心为1,那么我们统计了:
1->2,1->2->3,1->2->4
2->3,2->4
……
实际上我们想要的只是第1行的贡献,即我们只想合并子树与子树之间的贡献,对于子树内部的贡献,则留给下次统计子树时,然后,又因为每种合法答案必然通过某个重心,而我们不会重复统计重心且会将重心枚举完全,因此能够保证答案不重不漏。
但是上述代码却统计了子树内部的信息,即我们算以1为根的子树时,将3->2->4之类的贡献也一并算进去了,但是类似的贡献其实在下一次计算时也会算进去,因此我们要将这种非法答案减去。
再来一道题。
#BZOJ2152 聪聪可可
Description
聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:
由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。
聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。
Input
输入的第1行包含1个正整数n。
后面n-1行,每行3个整数x、y、w,表示x号点和y号点之间有一条边,上面的数是w。
Output
以即约分数形式输出这个概率(即“a/b”的形式,其中a和b必须互质。如果概率为1,输出“1/1”)。
Sample Input
5
1 2 1
1 3 2
1 4 1
2 5 3
Sample Output
13/25
Hint
【样例说明】
13组点对分别是(1,1) (2,2) (2,3) (2,5) (3,2) (3,3) (3,4) (3,5) (4,3) (4,4) (5,2) (5,3) (5,5)。
【数据规模】
对于30%的数据,n<=1000;
另有20%的数据,给出的树中每个节点的度不超过2;
对于100%的数据,n<=20000。
#include<bits/stdc++.h>
using namespace std;
#define Inc(i,L,r) for(register int i=(L);i<=(r);++i)
const int N = 2e4+10;
struct Edge{
int to[N<<1],next[N<<1],w[N<<1];
int cnt,h[N];
inline void add(int x,int y,int z){
next[++cnt]=h[x];
to[cnt]=y;
w[cnt]=z;
h[x]=cnt;
}
}e;
int n,G,sum,Minsiz,siz[N];
int ans,c[3],dis[N];
bool vst[N];
inline void init(){
scanf("%d",&n);
Inc(i,1,n-1){
int x,y,w;scanf("%d%d%d",&x,&y,&w);
e.add(x,y,w),e.add(y,x,w);
}
sum=n;
}
inline void FindG(int x,int fa){//找重心
int Maxsiz=0;
siz[x]=1;
for(int p=e.h[x];p;p=e.next[p])if(e.to[p]^fa&&!vst[e.to[p]]){
FindG(e.to[p],x);
siz[x]+=siz[e.to[p]];
Maxsiz=max(Maxsiz,siz[e.to[p]]);
}
Maxsiz=max(Maxsiz,sum-siz[x]);
if(Maxsiz<Minsiz)G=x,Minsiz=Maxsiz;
}
inline void FindD(int x,int fa,int dist){
dis[++dis[0]]=dist;
for(int p=e.h[x];p;p=e.next[p])if(e.to[p]^fa&&!vst[e.to[p]]){
FindD(e.to[p],x,dist+e.w[p]);
}
}
inline void Cal(int x,int dist,int cmd){
dis[0]=c[0]=c[1]=c[2]=0;
FindD(x,0,dist);
Inc(i,1,dis[0])++c[dis[i]%3];
ans+=(c[0]*c[0]+c[1]*c[2]*2)*cmd;
}
inline void stat(int x){
Minsiz=1<<30;
FindG(x,0);
vst[G]=1;
Cal(G,0,1);
for(int p=e.h[G];p;p=e.next[p])if(!vst[e.to[p]]){
Cal(e.to[p],e.w[p],-1);
sum=siz[e.to[p]];
stat(e.to[p]);
}
}
inline int gcd(int a,int b){
if(!b)return a;
return gcd(b,a%b);
}
int main(){
init();
stat(1);
printf("%d/%d\n",ans/gcd(ans,n*n),n*n/gcd(ans,n*n));
return 0;
}
发现了吗,点分治都是一个板子,所以这个玄乎的东西只要搞清楚怎么计数就好了(当然,你搞不清楚点分治计数也不一定搞得清楚啊)
最后还是总结一下点分治的套路(模板):
#define Inc(i,L,r) for(register int i=(L);i<=(r);++i)
const int N = 2e4+10;
struct Edge{
int to[N<<1],next[N<<1],w[N<<1];
int cnt,h[N];
inline void add(int x,int y,int z){
next[++cnt]=h[x];
to[cnt]=y;
w[cnt]=z;
h[x]=cnt;
}
}e;
int n,G,sum,Minsiz,siz[N];
int ans,dis[N];
bool vst[N];
inline void FindG(int x,int fa){//找重心
int Maxsiz=0;
siz[x]=1;
for(int p=e.h[x];p;p=e.next[p])if(e.to[p]^fa&&!vst[e.to[p]]){
FindG(e.to[p],x);
siz[x]+=siz[e.to[p]];
Maxsiz=max(Maxsiz,siz[e.to[p]]);
}
Maxsiz=max(Maxsiz,sum-siz[x]);
if(Maxsiz<Minsiz)G=x,Minsiz=Maxsiz;
}
inline void FindD(int x,int fa,int dist){
//……记录所需信息
/*dis[++dis[0]]=dist;
for(int p=e.h[x];p;p=e.next[p])if(e.to[p]^fa&&!vst[e.to[p]]){
FindD(e.to[p],x,dist+e.w[p]);
}*/
}
inline void Cal(int x,int dist,int cmd){
//初始化
FindD(x,0,dist);
//计算贡献
}
inline void stat(int x){
Minsiz=1<<30;
FindG(x,0);
vst[G]=1;
Cal(G,0,1);
for(int p=e.h[G];p;p=e.next[p])if(!vst[e.to[p]]){
Cal(e.to[p],e.w[p],-1);//减去贡献
sum=siz[e.to[p]];
stat(e.to[p]);
}
}
另外,必须明确每次点分治解决的严格经过这个点的路径= =不然会出现玄学错误的