点分治教程:
例题
给定一棵带权树,显然共有N*(N-1)/2条边,问:第k小的边边长多长?
N<=10000.
题解:
这道题直接上手做实在是太难了,需要逐步拆分。
首先,问题是第k小的边的边长,这个问题不好解,但是转换一下问题,二分第k小的边长T,然后判断这棵树中<=T的路径有多少条,这个能稍微好做一下,至少变成了一个统计性问题。
二分后题目变成:
给定一棵带权树,显然共有N*(N-1)/2条边,问:<=T的边有多少条(POJ1741)。
显然的,对于整个树的根来说,路径只有两种:
1.经过根的路径
2.不经过根的路径
显然,不经过根的所有路径,都单独属于某个根的子树中。
因此,我们完全可以这么做此题:
void solve(int i)//表示统计以i为根的这棵树中的满足题目要求的路径个数。
{
work(i); //统计所有经过根节点i的满足要求的路径个数。
delete(i); //把节点i从整棵树中删掉,这样i的所有儿子就形成了不同的子树
for (i的任何一个儿子j)
{
if (j属于i的某个子树中)
solve(j); //统计以j为根节点的子树中满足要求的路径个数
}
}
例如:共有一颗树,联通情况如下:
1
2————|————3
4——|——5 6——|——7
那么,首先调用solve(1),处理所有经过1的路径,然后把1删掉,调用solve(2),solve(3)分别处理,solve(2)处理完后调用了solve(4),solve(5),solve(3)以此类推。
这么做显然是明智的,把一个规模为n的问题,通过计算一个比较简单的子问题,转换成了两个规模为2n的问题。
如果计算这个比较简单的子问题的复杂度是nlogn的话,那么总的复杂度就是nlognlogn。
然而,这么做有一个反例:
1——2——3——4——5——6——7
首先调用solve(1),然后调用solve(2),然后调用solve(3)…
如此一来复杂度退化成了n*nlogn
为了避免这一种退化,我们可以人为的规定每棵子树的根,对于上述问题:
首先调用solve(4),然后两颗子树就是1——2——3,5——6——7,再分别调用solve(2),solve(6)。
这样,最多经过logn层,势必把所有的节点都处理完毕了。
而每棵子树的根很好选择,显然就是当前子树的重心!用两遍dfs可以求得。
solve的代码非常好写,如下:
void solve(int now) //表示solve以now为根的子树,此时now已经是重心
{
int u;
ans += work(now); //work(now)返回的是经过now的路径中,满足题意的数量。
done[now] = true; //把now从树中删除
for (int i=0; i//枚举和now相邻的每一个点u
if (!done[u = g[now][i].v]) //如果u没有被删除,说明在某一棵子树中。
{
f[0] = size = s[u];
getroot(u, root=0); //找到u所在子树的重心root
solve(root); //递归处理root
}
}
不会求树重心的移步这里
:http://blog.csdn.net/xdu_truth/article/details/9104629
现在的问题变成了如何写work(now),即在now所在的子树中,有多少条经过now的边长度<=T。
为了求这个问题,我们可以求出子树中now到所有点j的
路径长度dis[j],
从谁来 from[j](表示j是从now编号为from[j]的儿子走来的,例如1-2-3-4,from[4]=from[3]=from[2]=2)
那么,问题就变成了,对于每一个dis[j],有多少个dis[t]+dis[j]<=T,且from[j]!=from[t],方法非常多,此处不再赘述。
作业:
poj1741,hdu4812,codeforces161D,bzoj3697,bzoj2152。
1.求数的重心:
//sz[x]-->x的树大小,f[x]-->x最大子树的节点数;
void getrt(int x,int fa)//利用*sz,*f求重心
{
sz[x]=1;
f[x]=0;
for(int i=0;isize();i++)
{
int u=lin[x][i].x;
if(vis[u]||u==fa) continue;
getrt(u,x);
sz[x]+=sz[u];
f[x]=max(sz[u],f[x]);
}
f[x]=max(f[x],size-sz[x]);//! x最大子树的节点数=max(与此子树大小-f[x],f[x])
if(f[x]
2.solve函数
void solve(int x)
{
vis[x]=1;
cal(x);//每到题不同的地方。。。。
for(int i=0;isize();i++)
{
int u=lin[x][i].x;
if(vis[u]) continue;
f[0]=size=sz[u];
getrt(u,rt=0);
solve(rt);
}
}
3**.接下来的cal函数**
//一般的两个辅助数组
//pre[x] 代表到当前根的子树时,当前子树之前路径上值为x的方案数
//now[x] 代表到当前根的子树时,当前子树上路径上值为x的方案数
//dis[x]
void cal(int x)
{
for(int i=0;iint u=lin[x][i].x;
dis[u]=lin[x][i].y;
if(vis[u]) continue;
dfs(u,x);//dfs更新子树的now【】值
//ans1=。。。。。。。g[j]*ff[rev(j)];
//统计数答案数;
}
for(int i=0;i<3;i++) g[i]=0;
}
4.cal中的dfs
void getdis(int x,int fa)//一遍dfs求值+求重心的一点预处理sz[x],求子树大小size
{
sz[x]=1;
d.push_back(dis[x]);
for(int i=0;iint u=lin[x][i].x;
if(vis[u]||u==fa) continue;
// dis[u]=dis[x]+lin[x][i].y;
getdis(u,x);
sz[x]+=sz[u];
}
}
例1:poj 1741
http://blog.csdn.net/alps233/article/details/51392495
nlog n 计算对于每个子树的经过该子树的根的方案数;
即 求出子树中now到所有点j的
路径长度dis[j],*logn 的排序统计;.cal(x)统计答案,x子树中满足dis[x]+dix[y]<=K 的方案数;
**例2:**codeforces 161D] Distance in Tree
http://blog.csdn.net/alps233/article/details/51393322
在cal函数中引入 tmp[],cnt[],代表作到x子树时,当前层solve中之前子树《=k的dis个数,因为k<=500;O(n)计算,且不重复
例3: [bzoj 2152] 聪聪可可
http://blog.csdn.net/alps233/article/details/51396309
明显的树上的点分治,利用两个数组g[]表示搜当前根的子树时,当前子树之前的路径长x的方案数,ff表示当前子树路径长x方案数
*ans+=g[j]*ff[(3-j)%3]*2; //注意(1,1)合法,(1,2)(2,1)算两种;
例4:hdu4812
预处理逆元后cal中处理flag【ni【i】】的位置进行更新
if (path[j]*val[rt]%mod==K) getans(rt,id[j]);
int tmp=K*ni[path[j]*val[rt]%mod]%mod;
if(flag[tmp]!=ca) continue;
getans(F[tmp],id[j]);
例5
bzoj3697: 采药人的路径
枚举根节点的每个子树。用f[i][0…1],g[i][0…1]分别表示前面几个子树以及当前子树和为i的路径数目,0和1用于区分路径上是否存在前缀和为i的节点。
ans+=f[0][0] * g[0][0] + f [i][0] * g [-i][1] + f[i][1] * g[-i][0] + f[i][1] * g[-i][1]
//g[x][]代表到当前根的子树时,当前子树之前路径上值为x的方案数 0代表路径上有休息点,1代表无休息点的个数;
//f[x][] 代表到当前根的子树时,当前子树上路径上值为x的方案数 0代表路径上有休息点,1代表无休息点的个数;
其中i的范围[-d,d],d为当前子树的深度。
在每个子树 处更新并统计经过rt的方案数即可