P3806 - 点分治

折月煮酒 提交于 2019-12-02 06:43:05

题目链接:https://www.luogu.org/problem/P3806

思路:点分治是一种基于树的重心,统计树上路径的优秀算法。将树上的路径分为经过根节点和不经过根节点两种,对于前者, 我们用 

 表示结点 

 到根节点 

 的路径长度, 则 

 到

的路径长为 

对于后者,如果把根节点删掉,则可以生成若干颗以原根节点的儿子为根节点的子树,对于这些子树又可以分为经过根节点和不经过根节点两种。

所以点分治步骤大概是:

  1. 处理根节点的所有路径
  2. 删掉根节点
  3. 对生成的每颗子树的根节点重复1,2步骤

如果树退化成一条链, 那么递归层数就是

,总时间复杂度为

,显然不行。

所以我们要找树的重心,点分治过程中每次选取子树的重心为子树的根节点进行处理, 这样总的递归深度不会超过 

 , 整个点分治的时间复杂度也就保证了

最终点分治的步骤是这样的:

  1. 找到当前树的重心
  2. 处理根节点的所有路径
  3. 删掉根节点
  4. 对于生成所有的子树,重复以上步骤
 #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

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!