点分治用来统计树上路径。
点分治的核心是分治。
我们选一个点,统计过该点的路径。然后分治每一颗子树。
我们如果选择重心的话,可以证明,最多递归logn次。
因为选择重心,我们每一次递归的子树节点数都要小于原树的一半,所以log级别。
那么我们如果统计过一点的时间是T(x),则总时间为O(logn*T(n))
以聪聪可可这道题为例:(原题这里)
#include<bits/stdc++.h> #define N 20017 #define eho(x) for(int i=head[x];i;i=net[i]) using namespace std; int t[8]; int it,x,y,w,n,ans,sum,siz[N],head[N],vis[N],f[N],net[N<<1],fall[N<<1],tot,cost[N<<1]; #define sight(c) (‘0‘<=c&&c<=‘9‘) inline void read(int &x){ static char c; for (c=getchar();!sight(c);c=getchar()); for (x=0;sight(c);c=getchar())x=x*10+c-48; } int gcd(int x,int y) { if (!y) return x;return gcd(y,x%y); } inline void add(int x,int y,int w){ fall[++tot]=y; net[tot]=head[x]; head[x]=tot; cost[tot]=w; } void dfs(int x,int fa){ siz[x]=1;eho(x) if (fall[i]^fa&&!vis[fall[i]]) dfs(fall[i],x),siz[x]+=siz[fall[i]]; } void dfs2(int x,int fa){ f[x]=sum-siz[x]; eho(x) if (fall[i]^fa&&!vis[fall[i]]) f[x]=max(f[x],siz[fall[i]]),dfs2(fall[i],x); if (f[it]>f[x]) it=x; } void dfs3(int x,int fa,int to){ t[to]++; eho(x) if (fall[i]^fa&&!vis[fall[i]]) dfs3(fall[i],x,(to+cost[i])%3); } int cal(int x,int val) { t[1]=t[2]=t[0]=0; dfs3(x,0,val%3); return t[1]*t[2]*2+t[0]*t[0]; } void sol(int pos){ dfs(pos,0);sum=siz[pos];it=0; dfs2(pos,0);pos=it;ans+=cal(pos,0);vis[pos]=1; eho(pos)if (!vis[fall[i]]){ ans-=cal(fall[i],cost[i]); sol(fall[i]); } } int main () { read(n); for (int i=1;i<n;i++) { read(x),read(y),read(w);w%=3; add(x,y,w); add(y,x,w);} f[0]=n; sol(1); int t=gcd(ans,n*n); printf("%d/%d\n",ans/t,n*n/t); }
再说一个非常迷的事情:
有些std的solve :
void solve(int u) { ans+=calc(u,0); vis[u]=1;//将当前点标记 for(int i=h[u];i;i=e[i].next) { int v=e[i].to; if(vis[v]) continue; ans-=calc(v,e[i].w); root=0;//初始化根 sum=son[v];//初始化sum,注意这里的sum不一定是子树的大小 getroot(v,0);//找连通块的根 solve(root);//递归处理下一个连通块 } }
那么有一个问题,我们每次分出一个重心后,可能有一颗子树是向上的,但这样写就统计不出向上的那颗子树的正确的大小,但是我们的点分治只是跟时间复杂度有关,与答案无关,所以总是能跑出正确的答案,而多次尝试可以证明,这样用向下的子树大小来取代真实的子树大小,在一般数据下是没有关系的(由于向上的子树每次都期望按一定比例缩小,所以还是保证log级别的复杂度)。但我不知道会不会存在特殊数据使这种写法退化成O(N^2)。(有大佬知道的话请告诉我),但总之这种写法总是让新学者很迷(比如我),所以出来解释一下。