点分治
Posted wozaixuexi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了点分治相关的知识,希望对你有一定的参考价值。
1 概述
点分治是一种对树上符合某种条件的路径进行静态统计的算法。
当然,动态点分治(点分治树)是有的,但此篇文章暂不涉及。
这种算法的思想就是:对于一棵树,可以把树上的路径分为两类,一类是经过根结点的,一类是不经过根结点的。对于第二类路径,我们可以通过将根节点的每棵子树作为子问题递归处理,这样,我们只需要考虑如何处理第一种路径即可,使问题简化。
2 例题
2.0 题目链接
2.1 题目大意
给出一棵有N个点的树,树上的每条边都有一个权值,求长度不超过K的路径有多少条。
2.2 思路
经典的点分治问题。
我们先选一个结点作为根,然后开始处理上文中提到的“第一类路径”,即经过根节点的路径,其余路径递归处理。
设结点x到根的距离为dis[x],我们先对当前这棵树dfs一遍,将遍历到的结点都存在一个数组que中,然后按dis的值从小到大排序。接着,我们定义两个指针i和j。其中i从左到右扫描这个数组,j从右到左扫描这个数组。对于每一个i,我们都找出一个最靠右的j使得dis[que[i]]+dis[que[j]]≤K,此时可将j-i累加到答案中,直到i≥j为止。可以发现,j的位置只会越来越靠左,故这个统计答案过程可以在O(Nlog2N+N)的时间内实现。
值得注意的是,我们在选择让哪个点作为根时一定要选树的重心。这样的话,在每一次的递归分治中,结点的规模至少减半,可以保证最多只会进行log2N层点分治。倘若不选树的重心作为根,则点分治进行的层数无法保证,时间复杂度也极易退化。
每次选择树的重心作为根后,总时间复杂度即为O(Nlog22N)。
2.3 代码实现
#include<iostream> #include<cstdio> #include<algorithm> #include<cstring> using namespace std; struct edge { int last; int end; int weight; }e[20005]; //vis记录每个点是否被访问过 //que为存放结点的数组 //dis为每个结点到根的距离 //siz为每个结点的子树大小 //msiz为每个结点的所有子树中,最大的子树的大小 //siz和msiz都是用来求树的重心的 int n=0,k=0,ne=0,head=0,tail=0,note[10005],vis[10005],que[10005],dis[10005],siz[10005],msiz[10005]; void csh()//多组数据,记得初始化 { ne=head=tail=0; for(int i=0;i<20005;i++) e[i].last=e[i].end=e[i].weight=0; for(int i=0;i<10005;i++) note[i]=vis[i]=que[i]=dis[i]=siz[i]=msiz[i]=0; } void make_edge(int u,int v,int w) { ne++; e[ne].last=note[u]; e[ne].end=v; e[ne].weight=w; note[u]=ne; } bool cmp(int x,int y) { return dis[x]<dis[y]; } void dfs(int x) { siz[x]=vis[x]=1,msiz[x]=0; que[++tail]=x;//存储遍历到的结点 for(int i=note[x];i;i=e[i].last) if(!vis[e[i].end]) { dis[e[i].end]=dis[x]+e[i].weight; dfs(e[i].end); siz[x]+=siz[e[i].end]; msiz[x]=max(msiz[x],siz[e[i].end]); } vis[x]=0; } int calc(int l,int r) { sort(que+l,que+r+1,cmp);//先按dis的值从小到大排序 int ans=0; for(int i=l,j=r;i<j;i++)//两个指针一起扫 { while(i<j&&dis[que[i]]+dis[que[j]]>k) j--; ans+=j-i;//累加答案 } return ans; } int work(int x) { head=1,tail=0; dfs(x);//先dfs一遍(用于找树的重心) int sum=siz[x],misiz=msiz[x]; for(int i=1;i<=tail;i++)//找树的重心 if(max(msiz[que[i]],sum-msiz[que[i]])<misiz) misiz=max(msiz[que[i]],sum-msiz[que[i]]),x=que[i]; vis[x]=1;//标记这个点已被计算过,防止程序对重复的点进行点分治 int ans=0; head=1,tail=0; for(int i=note[x];i;i=e[i].last) if(!vis[e[i].end]) { dis[e[i].end]=e[i].weight; dfs(e[i].end);//收集结点 ans-=calc(head,tail);//先减去路径两端在同一棵子树的不合法路径对答案的贡献 head=tail+1; } dis[x]=0,que[++tail]=x; ans+=calc(1,tail);//计算路径两端在不同子树的路径对答案的贡献 for(int i=note[x];i;i=e[i].last) if(!vis[e[i].end]) ans+=work(e[i].end);//递归分治 return ans; } int main() { for(;;) { scanf("%d%d",&n,&k); if(n==0&&k==0) break; csh(); for(int i=1;i<=n-1;i++) { int u=0,v=0,w=0; scanf("%d%d%d",&u,&v,&w); make_edge(u,v,w); make_edge(v,u,w); } printf("%d ",work(1)); } return 0; }
2.4 小结
每道题计算路径对答案的贡献的方式都不一样,但点分治的总体框架与思想是不变的。
点分治这种算法还是要多做题,多总结才行。
3 练习题
4 参考资料
《算法竞赛进阶指南》(李煜东著)第0x45节。
LZZ的言传身教。
以上是关于点分治的主要内容,如果未能解决你的问题,请参考以下文章