题目链接:https://www.luogu.org/problem/P3806
思路:点分治是一种基于树的重心,统计树上路径的优秀算法。将树上的路径分为经过根节点和不经过根节点两种,对于前者, 我们用
表示结点 到根节点 的路径长度, 则 到的路径长为 ,对于后者,如果把根节点删掉,则可以生成若干颗以原根节点的儿子为根节点的子树,对于这些子树又可以分为经过根节点和不经过根节点两种。
所以点分治步骤大概是:
- 处理根节点的所有路径
- 删掉根节点
- 对生成的每颗子树的根节点重复1,2步骤
如果树退化成一条链, 那么递归层数就是
,总时间复杂度为,显然不行。所以我们要找树的重心,点分治过程中每次选取子树的重心为子树的根节点进行处理, 这样总的递归深度不会超过
层, 整个点分治的时间复杂度也就保证了。最终点分治的步骤是这样的:
- 找到当前树的重心
- 处理根节点的所有路径
- 删掉根节点
- 对于生成所有的子树,重复以上步骤
#include <bits/stdc++.h> using namespace std; const int maxn = 1e4+5; const int maxk = 1e7+5; struct E { int to, w, next; }Edge[maxn<<1]; int tot, head[maxn]; inline void AddEdge(int u, int v, int w) { Edge[tot] = (E){v, w, head[u]}; head[u] = tot++; } // rt记录重心,sum记录当前树大小,cnt是计数器 int n, m, rt, sum, cnt; // tmp记录算出的距离,siz记录子树大小,dis[i]为rt与i之间的距离 // maxp用于找重心,q用于记录所有询问 int tmp[maxn], siz[maxn], dis[maxn], maxp[maxn], q[105]; // judge[i]记录在之前子树中距离i是否存在,ans记录第k个询问是否存在,vis记录被删除的结点 bool judge[maxk], ans[105], vis[maxn]; // 找重心 void getrt(int u,int f) { siz[u] = 1, maxp[u] = 0; //maxp初始化为最小值 //遍历所有儿子,用maxp保存最大大小的儿子的大小 for(int i = head[u]; ~i; i = Edge[i].next) { int v = Edge[i].to; if(v == f || vis[v]) continue; //被删掉的也不要算 getrt(v, u); siz[u] += siz[v]; if(siz[v] > maxp[u]) maxp[u] = siz[v]; //更新maxp } maxp[u] = std::max(maxp[u], sum - siz[u]); //考虑u的祖先结点 if(maxp[u] < maxp[rt]) rt = u; //更新重心(最大子树大小最小) } // 计算各结点与根结点之间的距离并全部记录在tmp里 void getdis(int u, int f) { tmp[cnt++] = dis[u]; for(int i = head[u]; ~i; i = Edge[i].next) { int v = Edge[i].to; if(v == f || vis[v]) continue; dis[v] = dis[u] + Edge[i].w; getdis(v, u); } } // 处理经过根结点的路径 //! 注意judge数组要存放之前子树里存在的路径长度,排除折返路径的可能 void solve(int u) { queue <int> que; for(int i = head[u]; ~i ; i = Edge[i].next) { int v = Edge[i].to; if(vis[v]) continue; cnt = 0; //注意置零计数器 dis[v] = Edge[i].w; getdis(v, u); //把距离都处理出来 for(int j = 0; j < cnt; j++) //遍历所有距离 for(int k = 0; k < m; k++) //遍历所有询问 if(q[k] >= tmp[j]) //如果询问大于单条路径长度,那就有可能存在 ans[k] |= judge[q[k]-tmp[j]]; //如果能用两条路径拼出来,那就存在 for(int j = 0; j < cnt; j++) //把存在的单条路径长度标上true,供下个子树用 { que.push(tmp[j]); judge[tmp[j]] = true; } } while(!que.empty()) //清空judge数组,不要用memset { judge[que.front()]=false; que.pop(); } } //TODO 分治 void divide(int u) { vis[u] = judge[0] = true; //删除根结点 solve(u); //计算经过根结点的路径 for(int i = head[u]; ~i; i = Edge[i].next) //分治剩余部分 { int v = Edge[i].to; if(vis[v]) continue; maxp[rt=0] = sum = siz[v]; getrt(v,0); getrt(rt,0); divide(rt); } } int main() { memset(head,-1,sizeof(head)); scanf("%d%d", &n, &m); for(int i = 1; i < n; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); AddEdge(u,v,w); AddEdge(v,u,w); } for(int i = 0; i < m; i++) scanf("%d", &q[i]); maxp[0] = sum = n; //maxp[0]置为最大值(一开始rt=0) getrt(1, 0); //找重心 //! 此时siz数组存放的是以1为根时的各树大小,需要以找出的重心为根重算 getrt(rt, 0); //以重心为根再次更新siz[]数组 divide(rt); //找好重心就可以开始分治了 for(int i = 0; i < m; i++) { if(ans[i]) puts("AYE"); else puts("NAY"); } return 0; }
参考:https://www.bilibili.com/video/av69607337?from=search&seid=6282270604281613616
来源:https://blog.csdn.net/sugarbliss/article/details/102732038