zoukankan      html  css  js  c++  java
  • 【模版】【P3806】点分治

    (7.17)早就想学点分治了……今天状态不太在线,眯一会写篇笔记来理理思路。

    --------------------------------------------------------------------

      (静态)点分治是一种利用无根树性质暴力分治的思想,可以在O(nlogn)的复杂度下统计可带权树上的路径信息。

      像是这道例题,多组询问是否存在长度为k的路径,需要我们预处理出一个储存所有路径长度信息的桶。

      点分治的做法,就是选定一个合适的根节点,把树上的所有路径分成不重不漏的两部分来统计:

      1、经过根节点u的路径;

      2、在u某个子树中的路径。

      每次分治我们会统计出第一种路径的信息,然后递归进入u的每个子树,将第二种路径看作它的子树内的子问题来求解。

      首先,我们要选定一个合适的根节点开始分治。最理想的根节点要满足它的每个子树大小都基本一样大;于是我们就想起了重心这个好东西。

       无根树的重心u的性质:

        1、最大子树的大小最小。

        2、最大的子树大小小于等于树大小的一半。

      如果每次选定该子树的重心为根来进行分治,我们就可以保证递归的进行不超过logn层。

    1. void find_rt(int u, int pre) {  
    2.     size[u] = 1;  
    3.     int Mx = 0;  
    4.     for (int i = head[u]; i; i = edge[i].nxt) {  
    5.         int v = edge[i].to;  
    6.         if (v == pre || vis[v]) continue;  
    7.         find_rt(v, u);  
    8.         size[u] += size[v];  
    9.         Mx = max(Mx, size[v]);  
    10.     }  
    11.     Mx = max(Mx, Size - size[u]);  
    12.     if (Mx < Mn)   
    13.         root = u, Mn = Mx;  

    --------------------------------------------------------------------

      接下来是分治和统计的过程。O(nlogn)实际上是算法框架的复杂度,实际复杂度会随统计手段而改变。

      针对这道题来说,我们通过一次暴力dfs统计出当前子树中每个点的路径信息(包括根自身,深度为0),然后继续很暴力地两两组合路径,然后就出问题了……

      直接合并任意两条路径是行不通的,因为这两条路径可能来自u的同一个子节点v。此时我们得到的这条不合法路径的信息把u--->v的这条边统计了两次,所以我们要再遍历一遍它的每个子树,把这些不合法路径去掉。具体的操作可以看代码,用到了容斥原理。

    1. void dfs(int u, int pre, int depth) {  
    2.     chd[++tot] = depth;  //记录每个子节点深度
    3.     for (int i = head[u]; i; i = edge[i].nxt) {  
    4.         int v = edge[i].to;  
    5.         if (v == pre || vis[v])   
    6.             continue;  
    7.         dfs(v, u, depth + edge[i].w);  
    8.     }  
    9. }  
    10. void solve(int u, int extra, bool f) {  //第三个参数表示加减
    11.     tot = 0;  
    12.     dfs(u, 0, extra);  
    13.     if (f) {  
    14.         for (int i = 1; i <= tot; ++i)  
    15.             for (int j = i + 1; j <= tot; ++j)   
    16.                 ++ans[chd[i] + chd[j]];  
    17.     } else {  
    18.         for (int i = 1; i <= tot; ++i)  
    19.             for (int j = i + 1; j <= tot; ++j)   
    20.                 --ans[chd[i] + chd[j]];  
    21.     }  
    22. }  
    23. void divide(int u) {  //分治过程
    24.     vis[u] = true;  
    25.     solve(u, 0, 1); 
    26.     for (int i = head[u]; i; i = edge[i].nxt) {  
    27.         int v = edge[i].to;  
    28.         if (vis[v]) continue;  
    29.         solve(v, edge[i].w, 0);  //第二个参数为初始的深度,保证与以u为根算出的深度统一。
    30.         Mn = inf, Size = size[v];  
    31.         find_rt(v, 0);  
    32.         divide(root);  
    33.     }  
    34. }  

      大概不管是谁看到这里都想吐槽了:这个常数大到哪里去了?每回都要多跑一遍O(n)的搜索和O(n^2)的统计,虽然复杂度没有变,但是接受不能。

      实际上该题分治的过程有第二种写法,但是我还没有掌握,所以今天就先更新到这里。

    --------------------------------------------------------------

    (7.19)肝出了点分治的第二种写法。

      我们完全可以通过直接进行不重不漏的统计来避免容斥。处理u时,我们每次跑出点u的一个子树内的所有深度,把它们计入子树深度信息的同时,与之前得到的别的子树的信息组合,统计答案。注意这里要把点u本身计入这个child数组中,深度为0,这样就涵盖了路径结尾在u的情况。

    代码:

    1. int chd[maxn], temp, tot;  
    2. void dfs(int u, int pre, int d) {  
    3.     chd[++tot] = d;  
    4.     for (int i = head[u]; i; i = edge[i].nxt) {  
    5.         int v = edge[i].to;  
    6.         if (v == pre || vis[v])  
    7.             continue;  
    8.         dfs(v, u, d + edge[i].w);  
    9.     }  
    10. }  
    11. void solve(int u, int extra) {  
    12.     temp = tot;  
    13.     dfs(u, 0, extra);  
    14.     for (register int i = temp + 1; i <= tot; ++i)     
    15.         for (register int j = 1; j <= temp; ++j)  
    16.             ++ans[chd[i] + chd[j]];  
    17. }  
    18. void divide(int u) {  
    19.     vis[u] = true;  
    20. //  memset(chd, 0, sizeof(chd));//直接覆盖原数组信息即可,不用拷贝也不用清空   
    21.     chd[1] = 0;  
    22.     tot = 1; //算上自己   
    23.     for (int i = head[u]; i; i = edge[i].nxt) {  
    24.         int v = edge[i].to;  
    25.         if (vis[v]) continue;  
    26.         solve(v, edge[i].w);  
    27.     }  
    28.     for (int i = head[u]; i; i = edge[i].nxt) {  
    29.         int v = edge[i].to;  
    30.         if (vis[v]) continue;  
    31.         Mn = inf, Size = size[v];  
    32.         find_rt(v, 0);  
    33.         divide(root);  
    34.     }  
    35. }  

      这段代码的实测效率比上一种写法快了一倍(开O2快10倍@w@)。下面放上完整的代码:

    1. #include <iostream>  
    2. #include <cstring>  
    3. #include <cstdio>  
    4. #define maxn 10010  
    5. const int inf(0x3fffffff);  
    6. using namespace std;  
    7. template <typename T>  
    8. void read(T &x) {  
    9.     x = 0;  
    10.     int f = 1;  
    11.     char ch = getchar();  
    12.     while (!isdigit(ch)) {  
    13.         if (ch == '-')  
    14.             f = -1;  
    15.         ch = getchar();  
    16.     }  
    17.     while (isdigit(ch)) {  
    18.         x = x * 10 + (ch ^ 48);  
    19.         ch = getchar();  
    20.     }  
    21.     x *= f;  
    22.     return;  
    23. }  
    24.   
    25. int n, m;   
    26. int head[maxn], top;  
    27. struct E {  
    28.     int to, nxt, w;  
    29. } edge[maxn << 1];  
    30. inline void insert(int u, int v, int w) {  
    31.     edge[++top] = (E) {v, head[u], w};  
    32.     head[u] = top;  
    33. }  
    34. int Mn, root, Size, size[maxn], ans[10000010];  
    35. bool vis[maxn];  
    36. void find_rt(int u, int pre) {  
    37.     size[u] = 1;  
    38.     int Mxson = 0;  
    39.     for (int i = head[u]; i; i = edge[i].nxt) {  
    40.         int v = edge[i].to;  
    41.         if (vis[v] || v == pre)  
    42.             continue;  
    43.         find_rt(v, u);  
    44.         size[u] += size[v];  
    45.         if (size[v] > Mxson)  
    46.             Mxson = size[v];  
    47.     }  
    48.     Mxson = max(Mxson, Size - size[u]);  
    49.     if (Mxson < Mn)  
    50.         root = u, Mn = Mxson;  
    51. }  
    52. int chd[maxn], temp, tot;  
    53. void dfs(int u, int pre, int d) {  
    54.     chd[++tot] = d;  
    55.     for (int i = head[u]; i; i = edge[i].nxt) {  
    56.         int v = edge[i].to;  
    57.         if (v == pre || vis[v])  
    58.             continue;  
    59.         dfs(v, u, d + edge[i].w);  
    60.     }  
    61. }  
    62. void solve(int u, int extra) {  
    63.     temp = tot;  
    64.     dfs(u, 0, extra);  
    65.     for (register int i = temp + 1; i <= tot; ++i)     
    66.         for (register int j = 1; j <= temp; ++j)  
    67.             ++ans[chd[i] + chd[j]];  
    68. }  
    69. void divide(int u) {  
    70.     vis[u] = true;  
    71. //  memset(chd, 0, sizeof(chd));//直接覆盖原数组信息即可,不用拷贝也不用清空   
    72.     chd[1] = 0;  
    73.     tot = 1; //算上自己   
    74.     for (int i = head[u]; i; i = edge[i].nxt) {  
    75.         int v = edge[i].to;  
    76.         if (vis[v]) continue;  
    77.         solve(v, edge[i].w);  
    78.     }  
    79.     for (int i = head[u]; i; i = edge[i].nxt) {  
    80.         int v = edge[i].to;  
    81.         if (vis[v]) continue;  
    82.         Mn = inf, Size = size[v];  
    83.         find_rt(v, 0);  
    84.         divide(root);  
    85.     }  
    86. }  
    87. int main() {  
    88. //  freopen("testdata.in.txt", "r", stdin);  
    89. //  freopen("testdata.out", "w", stdout);  
    90.     read(n), read(m);  
    91.     int u, v, w, k;  
    92.     for (register int i = 1; i < n; ++i) {  
    93.         read(u), read(v), read(w);  
    94.         insert(u, v, w), insert(v, u, w);  
    95.     }  
    96.     Size = n, Mn = inf;  
    97.     find_rt(1, 0);  
    98.     divide(root);  
    99.     for (register int i = 1; i <= m; ++i) {  
    100.         read(k);  
    101.         puts(ans[k] ? "AYE" : "NAY");  
    102.     }  
    103.     return 0;  
    104. }  

          由于通过枚举每一条可行路径来n^2进行统计,这种写法有很大的局限性。例如【P4178】Tree 这道题,直接枚举统计会爆炸,需要排序和双指针扫描的技巧来成段统计可行路径。在大多数情况下还是需要使用容斥去重的方法进行点分治。

  • 相关阅读:
    爬取英雄联盟所有英雄皮肤
    Python xlrd模块读取Excel表中的数据
    H5的接口测试方式
    接口自动化
    DbUtils入门之QueryRunner
    常用注解
    修改Git下Git Bash开始键的默认起始路径
    SVN
    IDEA 2018 安装激活破解方法
    JVM原理(Java代码编译和执行的整个过程+JVM内存管理及垃圾回收机制)
  • 原文地址:https://www.cnblogs.com/TY02/p/11203163.html
Copyright © 2011-2022 走看看