一篇自己都看不懂的点分治&点分树学习笔记

时间:2024-01-21 13:04:32

淀粉质点分治可真是个好东西

Part A.点分治

众所周知,树上分治算法有$3$种:点分治、边分治、链分治(最后一个似乎就是树链剖分),它们名字的不同是由于分治方式的不同的。点分治,顾名思义,每一次选择一个点进行分治,对于树上路径统计类型的问题有奇效,思路很好理解,只是码量有些烦人

先来看一道模板题:CF161D

至于为什么我没有放Luogu模板题是因为那道题只会写$O(n^2logn)$的算法(然而跑得过是因为跑不满)

这道题要求在$N$个点的树上找距离为$K$的点对的数量。

因为我们是来学点分治的,所以我们考虑点分治。我们每一次选择一个分治中心,那么以这一个分治中心为根,这棵树就会有若干子树。这棵树上的路径被分为了两种:

①经过分治中心

②没有经过分治中心,也就是说这条路径在以当前分治中心为根的一棵子树内

我们可以递归解决②对应的问题,也就是说我们只要解决当前树的①问题

考虑每一次选择一棵子树对其进行深度优先搜索,开一个桶记录之前经过的子树中每一种路径长度对应的路径数量(一个小注明:路径指的是当前分治中心到达子树中某一个点的路径,下同)。每一次找到一条长度为$L$的路径之后,它对答案的贡献就是之前搜索过的子树中长度为$K-L$的路径的数量,因为这一条路径可以与这一些路径中的每一条拼接形成长度为$K$且经过当前分治中心的路径。在一棵子树遍历完了之后,再将这一棵子树的路径放入桶内。注意:不能找到一条路径就放进桶里面,因为这样可能会导致同一棵子树的两条路径被拼接并计入答案,但实际上它们之间的树上路径属于②,不应该在当前分治中心被统计到。当前分治中心解决之后,清空桶中元素,分治解决以当前分治中心为根的子树上的路径。

当然,你会发现一个问题:如果给出了一条链,结果你每一次选择的分治中心都是链两端的点,那复杂度不轻松卡成$O(n^2)$???

然而智慧的你不会让出题人这么轻松地卡掉你,我们考虑每一次选择一个点,以它为根时,子树大小尽量平均,也就是说最大的子树要尽量的小

那么我们当然会选择——树的重心

因为树的重心的优雅性质(以它为根的子树的大小不超过当前树大小的$\frac{1}{2}$),我们每一次分治下去的子树的大小都至少会减半,也就保证了$O(nlogn)$的复杂度。

代码在下面(虽然我写的是双指针统计路径条数,这一种方法会在下面的那道题目里提到)

 #include<bits/stdc++.h>
 #define MAXN 50001
 using namespace std;
 inline int read(){
     ;
     char c = getchar();
     while(!isdigit(c))
         c = getchar();
     ) + a + (c ^ ') , c = getchar();
     return a;
 }
 struct Edge{
     int end , upEd;
 }Ed[MAXN << ];
 struct node{
     int in , wei;
     bool operator <(node a){return wei < a.wei;}
 }Node[MAXN];
 int head[MAXN] , size[MAXN] , cnt[MAXN];
 int nowSize , N , K , minN , pos , cntEd , cntNode;
 long long ans;
 bool vis[MAXN];
 inline int max(int a , int b){return a > b ? a : b;}

 inline void addEd(int a , int b){
     Ed[++cntEd].end = b;
     Ed[cntEd].upEd = head[a];
     head[a] = cntEd;
 }
 inline void addNode(int a , int b){
     Node[++cntNode].in = a;
     Node[cntNode].wei = b;
 }

 void getSize(int k){
     nowSize++;
     vis[k] = ;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])
             getSize(Ed[i].end);
     vis[k] = ;
 }

 void getZX(int k){
     ;
     size[k] = vis[k] = ;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end]){
             getZX(Ed[i].end);
             maxSize = max(maxSize , size[Ed[i].end]);
             size[k] += size[Ed[i].end];
         }
     if(minN > (maxSize = max(maxSize , nowSize - size[k]))){
         minN = maxSize;
         pos = k;
     }
     vis[k] = ;
 }

 void dfs(int k , int in , int len){
     if(len > K)    return;
     addNode(in , len);
     vis[k] = ;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])
             dfs(Ed[i].end , );
     vis[k] = ;
 }

 void solve(int dir){
     nowSize = ;
     cntNode = ;
     minN = N + ;
     getSize(dir);
     getZX(dir);
     vis[pos] = ;
     ;
     for(int i = head[pos] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])
             dfs(Ed[i].end , ++);
     sort(Node +  , Node + cntNode + );
      , r = cntNode , p = cntNode , count = ;
     ;
     while(l < r){
          || Node[l].wei != Node[l - ].wei){
             while(r > p)
                 cnt[Node[r--].in]--;
             count = ;
             while(l < r && Node[l].wei + Node[r].wei > K)
                 r--;
             if(l >= r)    break;
             p = r;
             while(l < p && Node[l].wei + Node[p].wei == K){
                 cnt[Node[p].in]++;
                 count++;
                 f = ;
                 p--;
             }
         }
         ans += count - cnt[Node[l].in];
         if(p == l){
             cnt[Node[++p].in]--;
             count--;
         }
         l++;
     }
     for(int i = head[pos] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])
             solve(Ed[i].end);
 }
 int main(){
     N = read();
     K = read();
      ; i < N ; ++i){
         int a = read() , b = read();
         addEd(a , b);
         addEd(b , a);
     }
     solve();
     cout << ans;
     ;
 }

CF161D

再来一题:Tree

咦这题的等于$K$怎么变成小于等于$K$了

那么我们就不能使用桶了。而使用线段树等数据结构码量又会增大不少,我们可不可以用更优秀的方法解决呢?当然有。

每一次分治时,我们考虑将路径存下来,并按照长度从小到大排序,然后使用两个指针$L,R$来扫描路径数组并获取答案。

可以知道,当$L$在不断向右移动的时候,满足$len_L + len_R \leq K$的最大的$R$是单调递减的,所以可以直接调整$R$满足要求。调整了$R$之后,那么我们的答案就是$R-L$...

等等,我们没有考虑同一子树,所以我们还需要存下每一条路径的来源是哪一棵子树,用桶存好$L+1$到$R$之间每一个来源的数量,每一次$L$和$R$移动的时候维护这个桶,那么实际贡献的答案就是$R-L-\text{L+1到R中与L来源相同的路径的数量}$。

我们每一次分治的复杂度就是$O(\text{分治区域大小} log \text{分治区域大小})$的,总复杂度是$O(nlog^2n)$。如果写基数排序之类的东西的话复杂度就是$O(nlogn)$

代码在这里(之前把代码放成Race的了,现已Update)

 // luogu-judger-enable-o2
 #include<bits/stdc++.h>
 #define MAXN 40001
 using namespace std;
 struct Edge{
     int end , w , upEd;
 }Ed[MAXN << ];
 struct node{
     int in , len;
 }Node[MAXN];
 int cntEd , cntNode , N , nowSize , minN , pos , ans , K;
 int cnt[MAXN] , size[MAXN] , head[MAXN];
 bool vis[MAXN];

 inline bool cmp(node a , node b){return a.len < b.len;}

 inline void add(int a , int b , int c){
     Ed[++cntEd].w = c;    Ed[cntEd].end = b;
     Ed[cntEd].upEd = head[a];    head[a] = cntEd;
 }

 inline void pushNode(int len , int in){
     Node[++cntNode].in = in;    Node[cntNode].len = len;    cnt[in]++;
 }

 inline int min(int a , int b){return a < b ? a : b;}
 inline int max(int a , int b){return a > b ? a : b;}

 void getSize(int k){
     nowSize++;
     vis[k] = ;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])    getSize(Ed[i].end);
     vis[k] = ;
 }

 void findZX(int k){
     ;
     vis[k] = size[k] = ;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end]){
             findZX(Ed[i].end);
             maxSize = max(maxSize , size[Ed[i].end]);
             size[k] += size[Ed[i].end];
         }
     if(minN > (maxSize = max(maxSize , nowSize - size[k]))){
         minN = maxSize;
         pos = k;
     }
     vis[k] = ;
 }

 void dfs(int k , int len , int in){
     vis[k] = ;
     pushNode(len , in);
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])    dfs(Ed[i].end , len + Ed[i].w , in);
     vis[k] = ;
 }

 void solve(int dir){
     nowSize = cntNode = ;
     minN = N + ;
     getSize(dir);
     findZX(dir);
     pushNode( , );
     cnt[] = ;
     vis[pos] = ;
     ;
     for(int i = head[pos] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])    dfs(Ed[i].end , Ed[i].w , ++q);
     sort(Node +  , Node + cntNode +  , cmp);
      , r = cntNode;
     while(l < r){
         while(Node[r].len + Node[l].len > K && l < r)
             cnt[Node[r--].in]--;
         if(l < r){
             ans += r - l - cnt[Node[l].in];
             cnt[Node[++l].in]--;
         }
     }
     for(int i = head[pos] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])    solve(Ed[i].end);
 }

 int main(){
     scanf("%d" , &N);
      ; i < N ; i++){
         int a , b , c;
         scanf("%d%d%d" , &a , &b , &c);
         add(a , b , c);    add(b , a , c);
     }
     scanf("%d" , &K);
     solve();
     cout << ans;
     ;
 }

Tree

PS:因为Itst实在是太菜了,所以搞了好多次不晓得是$O(nlogn)$还是$O(nlog^2n)$,但实际上似乎是$O(nlog^2n)$的,因为分治式子是$T(n) = 2T(\frac{n}{2}) + O(nlogn)$,设$k = logn$,全部加起来有$T(n) = nlogn + 2\frac{n}{2}(logn - 1) + 4\frac{n}{4}(logn - 2) + ... + 2^k \frac{n}{2^k} (logn - k) = nlog^2n - nlogn = nlog^2n$

然后放几道练习题:

基础(比较裸就没有讲什么了):

Luogu点分治模板

 #include<bits/stdc++.h>
 #define MAXN 10001
 using namespace std;
 struct Edge{
     int end , len , upEd;
 }Ed[MAXN << ];
 struct node{
     int in , len;
 }Node[MAXN];
 int size[MAXN] , N , head[MAXN] , pos , ans , cnt , nowSize , cntNode , num[MAXN];
 ];

 inline int min(int a , int b){return a < b ? a : b;}
 inline int max(int a , int b){return a > b ? a : b;}

 inline void add(int a , int b , int c){
     Ed[++cnt].end = b;    Ed[cnt].upEd = head[a];    Ed[cnt].len = c;    head[a] = cnt;
 }

 inline void push_node(int len , int in){
     Node[++cntNode].in = in;    Node[cntNode].len = len;
 }

 void getSize(int k){
     vis[k] = ;
     nowSize++;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])    getSize(Ed[i].end);
     vis[k] = ;
 }

 void findZX(int k){
     ;
     vis[k] = size[k] = ;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end]){
             findZX(Ed[i].end);
             size[k] += size[Ed[i].end];
             maxSize = max(maxSize , size[Ed[i].end]);
         }
     maxSize = max(maxSize , nowSize - size[k]);
     if(ans > maxSize){
         ans = maxSize;
         pos = k;
     }
     vis[k] = ;
 }

 void dfs(int k , int len , int in){
     vis[k] = ;
     push_node(len , in);
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])    dfs(Ed[i].end , len + Ed[i].len , in);
     vis[k] = ;
 }

 void solve(int dir){
     ans = N + ;
     nowSize = cntNode = ;
     getSize(dir);
     findZX(dir);
     push_node( , );
     vis[pos] = ;
     ;
     for(int i = head[pos] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])    dfs(Ed[i].end , Ed[i].len , ++cnt);
      ; i <= cntNode ; i++)
          ; j <= cntNode ; j++)
             ;
     for(int i = head[pos] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])    solve(Ed[i].end);
 }

 int main(){
     int M;
     scanf("%d%d" , &N , &M);
      ; i < N ; i++){
         int a , b , c;
         scanf("%d%d%d" , &a , &b , &c);
         add(a , b , c);    add(b , a , c);
     }
     solve();
     while(M--){
         int K;
         scanf("%d" , &K);
         puts(haveAns[K] ? "AYE" : "NAY");
     }
     ;
 }

点分治模板

聪聪可可

 #include<bits/stdc++.h>
 #define MAXN 200001
 using namespace std;
 struct Edge{
     int end , len , upEd;
 }Ed[MAXN];
 ] , now[] , nowSize , ans , pos , minN;
 int head[MAXN] , size[MAXN];
 bool vis[MAXN];

 inline void add(int a , int b , int c){
     Ed[++cnt].end = b;    Ed[cnt].upEd = head[a];    Ed[cnt].len = c;    head[a] = cnt;
 }

 inline int max(int a , int b){return a > b ? a : b;}

 inline int gcd(int a , int b){
      ? b : gcd(b , a % b);
 }

 void getSize(int k){
     vis[k] = ;
     nowSize++;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])    getSize(Ed[i].end);
     vis[k] = ;
 }

 void findZX(int k){
     ;
     vis[k] = size[k] = ;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end]){
             findZX(Ed[i].end);
             maxSize = max(maxSize , size[Ed[i].end]);
             size[k] += size[Ed[i].end];
         }
     if(minN > (maxSize = max(maxSize , nowSize - size[k]))){
         minN = maxSize;
         pos = k;
     }
     vis[k] = ;
 }

 void dfs(int k , int len){
     now[len]++;
     vis[k] = ;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         );
     vis[k] = ;
 }

 void solve(int dir){
     nowSize = ;
     minN = N + ;
     getSize(dir);
     findZX(dir);
     ] = vis[pos] = ;
     now[] = now[] = now[] = ] = ] = ;
     for(int i = head[pos] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end]){
             dfs(Ed[i].end , Ed[i].len % );
              ; j <  ; j++)
                 ans +=  - j) % ] * ;
              ; j <  ; j++){
                 in[j] += now[j];
                 now[j] = ;
             }
         }
     for(int i = head[pos] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])    solve(Ed[i].end);
 }

 int main(){
     scanf("%d" , &N);
     ans += N;
      ; i < N ; i++){
         int a , b , c;
         scanf("%d%d%d" , &a , &b , &c);
         add(a , b , c);    add(b , a , c);
     }
     solve();
     int t = gcd(N * N , ans);
     cout << ans / t << '/' << N * N / t;
     ;
 }

聪聪可可

Race

 #include<bits/stdc++.h>
 #define MAXN 200001
 #define ll long long
 using namespace std;
 inline ll read(){
     ll a = ;
     char c = getchar();
     ;
     while(!isdigit(c)){
         ;
         c = getchar();
     }
     ) + (a << ) + (c ^ ') , c = getchar();
     return f ? -a : a;
 }

 struct Edge{
     int end , w , upEd;
 }Ed[MAXN << ];
 struct node{
     int len , in;
     ll wei;
 }Node[MAXN];
 int head[MAXN] , size[MAXN];
 bool vis[MAXN];
 int N , K , nowSize , minN , pos , ans , cntEd , cntNode;

 bool cmp(node a , node b){return a.wei < b.wei;}

 inline int max(int a , int b){return a > b ? a : b;}
 inline int min(int a , int b){return a < b ? a : b;}

 inline void addEd(int a , int b , ll c){
     Ed[++cntEd].end = b;    Ed[cntEd].w = c;    Ed[cntEd].upEd = head[a];    head[a] = cntEd;
 }

 inline void addNode(int in , int len , ll wei){
     Node[++cntNode].in = in;    Node[cntNode].len = len;
     Node[cntNode].wei = wei;
 }

 void getSize(int k){
     nowSize++;
     vis[k] = ;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])    getSize(Ed[i].end);
     vis[k] = ;
 }

 void findZX(int k){
     ;
     size[k] = vis[k] = ;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end]){
             findZX(Ed[i].end);
             maxSize = max(maxSize , size[Ed[i].end]);
             size[k] += size[Ed[i].end];
         }
     if(ans > (maxSize = max(maxSize , nowSize - size[k]))){
         ans = maxSize;
         pos = k;
     }
     vis[k] = ;
 }

 void dfs(int k , int in , int len , int w){
     addNode(in , len , w);
     vis[k] = ;
     for(int i = head[k] ; i ; i = Ed[i].upEd)
          , w + Ed[i].w);
     vis[k] = ;
 }

 void solve(int k){
     cntNode = nowSize = ;
     ans = 0x3f3f3f3f;
     getSize(k);
     findZX(k);
     vis[pos] = ;
     addNode( ,  , );
     ;
     for(register int i = head[pos] ; i ; i = Ed[i].upEd)
          , Ed[i].w);
     sort(Node +  , Node + cntNode +  , cmp);
     register  , r = cntNode;
     while(l < r){
         while(Node[l].wei + Node[r].wei > K && l < r)    r--;
         int p = r;
         while(Node[l].wei + Node[p].wei == K && l < p){
             if(Node[l].in != Node[p].in)
                 minN = min(minN , Node[l].len + Node[p].len);
             p--;
         }
         l++;
     }
     for(register int i = head[pos] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])    solve(Ed[i].end);
 }

 int main(){
     N = read();    K = read();
      ; i < N ; i++){
         int a = read() , b = read();
         ll c = read();
         addEd(a , b , c);    addEd(b , a , c);
     }
     minN = 0x3f3f3f3f;
     solve();
     cout << (minN ==  : minN);
     ;
 }

Race

较难:(Solution更新中)

快递员 Sol

树的难题 Sol

树上游戏

重建计划 Sol

B.动态点分治(点分树)

什么?点分治还能带修改?Of course!

我们可以发现:根据点分治,我们可以构建出一棵树,在点分治过程中,如果从$solve(a)$递归到了$solve(b)$,就令$a$所在分治区域的重心为$b$所在分治区域重心的父亲,这样我们就可以构造出点分树。点分树有几个优美的性质:

$a.$点分树的高度不超过$O(logn)$,因为点分治的递归深度不会超过$logn$

$b.$点分树上某个点的祖先(包括它自己)在点分治时的分治范围必定包括了这个点,而其他点的分治范围一定不会包含这个点。

$c.$点分树上某个点的儿子一定在这一个点的分治范围的子树中(废话)

这个性质告诉我们:如果在点分树上进行修改,只需要修改它到根的一条链,修改点数不会多于$logn$。

具体来说,看一道题:捉迷藏; 加强版:Qtree4

我们就说$Qtree4$的做法吧,毕竟捉迷藏是边权为$1$的特殊版本。

我们构建好点分树,考虑如何在点分树上维护答案。我们需要支持插入、删除和查询最大值、次大值,考虑使用堆+懒惰堆思想进行维护。

我们对每一个点维护一个堆$heap1$,维护当前节点对应的分治范围内的路径的最大值和次大值。但我们又会面对与静态点分治一样的问题:可能来自当前节点的同一个子树的一条路径在当前节点贡献答案。所以我们对于每一个节点还要维护当前节点对应的分治范围内的路径到达当前节点在点分树上的父亲的路径长度的堆$heap2$,这样父亲在转移时就可以直接取它的所有儿子的$heap2$的最大值放入自己对应的$heap1$中,统计答案的时候把它的$heap2$的最大值从它的父亲的$heap1$中删掉,就可以避免了重复的计算。然后我们在全局维护一个堆$heap3$来维护全局的答案,每一次产生新的答案就进行维护。

那么我们每一次翻转一个节点的颜色的时候,就在点分树上暴跳父亲,并维护好$heap1,heap2,heap3$。初始化的时候也暴跳父亲。复杂度$O(nlog^2n)$,在$Qtree4$上有一些卡常,给出一种优化方式:在删除的时候,不要在懒惰堆中加入一个元素就尝试删除答案堆,而是在询问的时候进行,这样可以降低常数。

注意一个细节:如果某一个节点可以被计入路径中,在它对应的$heap2$中是需要插入两个$0$的(表示自己与自己匹配或者自己与儿子匹配),这样子的答案才是正确的。

代码

 #include<bits/stdc++.h>
 #define INF 0x3f3f3f3f
 //This code is written by Itst
 using namespace std;

 inline int read(){
     ;
     ;
     char c = getchar();
     while(c != EOF && !isdigit(c)){
         if(c == '-')
             f = ;
         c = getchar();
     }
     while(c != EOF && isdigit(c)){
         a = (a << ) + (a << ) + (c ^ ');
         c = getchar();
     }
     return f ? -a : a;
 }

 ;
 struct Edge{
     int end , upEd;
 }Ed[MAXN << ];
 ] , dis[MAXN][] , dep[MAXN] , size[MAXN] , ST[][MAXN << ] , fir[MAXN] , logg2[MAXN << ];
 int nowSize , minSize , minInd , ts , N , cntEd;
 bool vis[MAXN];
 struct pq{
     priority_queue < int > now , del;
     inline void maintain(){
         while(!del.empty() && del.top() == now.top()){
             del.pop();
             now.pop();
         }
     }
     inline void push(int x){
         now.push(x);
     }
     inline void pop(int x){
         del.push(x);
         maintain();
     }
     inline int top(){
         return now.empty() ? -INF : now.top();
     }
     inline int sec(){
         if(now.empty())
             return -INF;
         int t = now.top();
         now.pop();
         maintain();
         int p = now.empty() ? -INF : now.top();
         now.push(t);
         return p;
     }
 }ans , cur[MAXN] , ch[MAXN];

 inline void addEd(int a , int b){
     Ed[++cntEd].end = b;
     Ed[cntEd].upEd = head[a];
     head[a] = cntEd;
 }

 void init_dfs(int now , int fa){
     fir[now] = ++ts;
     ST[][ts] = now;
     dep[now] = dep[fa] + ;
     for(int i = head[now] ; i ; i = Ed[i].upEd)
         if(Ed[i].end != fa){
             init_dfs(Ed[i].end , now);
             ST[][++ts] = now;
         }
 }

 inline int cmp(int a , int b){
     return dep[a] < dep[b] ? a : b;
 }

 void init_st(){
     logg2[] = -;
      ; i <= N <<  ; ++i)
         logg2[i] = logg2[i >> ] + ;
      ;  << i <= N <<  ; ++i)
          ; j + ( << i) -  <= N <<  ; ++j)
             ST[i][j] = cmp(ST[i - ][j] , ST[i - ][j + ( << (i - ))]);
 }

 inline int LCA(int x , int y){
     x = fir[x];
     y = fir[y];
     if(x < y)
         swap(x , y);
     ];
      << t) + ]);
 }

 void getSize(int x){
     vis[x] = ;
     ++nowSize;
     for(int i = head[x] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])
             getSize(Ed[i].end);
     vis[x] = ;
 }

 void getRoot(int x){
     vis[x] = size[x] = ;
     ;
     for(int i = head[x] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end]){
             getRoot(Ed[i].end);
             maxN = max(maxN , size[Ed[i].end]);
             size[x] += size[Ed[i].end];
         }
     maxN = max(maxN , nowSize - size[x]);
     if(maxN < minSize){
         minSize = maxN;
         minInd = x;
     }
     vis[x] = ;
 }

 inline int getLen(int x , int y){
     );
 }

 int init_dfz(int x , int pre){
     nowSize = ;
     minSize = INF;
     getSize(x);
     getRoot(x);
     x = minInd;
     vis[x] = ;
     fa[x][] = pre;
      , p = x ; fa[x][i] ; p = fa[x][i++]){
         dis[x][i] = getLen(x , fa[x][i]);
         fa[x][i + ] = fa[fa[x][i]][];
         ch[p].push(dis[x][i]);
     }
     for(int i = head[x] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])
             cur[x].push(ch[init_dfz(Ed[i].end , x)].top());
     cur[x].push();
     cur[x].push();
     ans.push(cur[x].top() + cur[x].sec());
     vis[x] = ;
     return x;
 }

 inline void init(){
     init_dfs( , );
     init_st();
     init_dfz( , );
 }

 inline void modify(int x){
     vis[x] ^= ;
     if(vis[x]){
         ans.pop(cur[x].top() + cur[x].sec());
         cur[x].pop();
         cur[x].pop();
         ans.push(cur[x].top() + cur[x].sec());
         int p = x;
          ; fa[x][i] ; p = fa[x][i++]){
             ans.pop(cur[fa[x][i]].top() + cur[fa[x][i]].sec());
             cur[fa[x][i]].pop(ch[p].top());
             ch[p].pop(dis[x][i]);
             cur[fa[x][i]].push(ch[p].top());
             ans.push(cur[fa[x][i]].top() + cur[fa[x][i]].sec());
         }
     }
     else{
         ans.pop(cur[x].top() + cur[x].sec());
         cur[x].push();
         cur[x].push();
         ans.push(cur[x].top() + cur[x].sec());
         int p = x;
          ; fa[x][i] ; p = fa[x][i++]){
             ans.pop(cur[fa[x][i]].top() + cur[fa[x][i]].sec());
             cur[fa[x][i]].pop(ch[p].top());
             ch[p].push(dis[x][i]);
             cur[fa[x][i]].push(ch[p].top());
             ans.push(cur[fa[x][i]].top() + cur[fa[x][i]].sec());
         }
     }
 }

 inline int query(){
      ? - : ans.top();
 }

 inline char getc(){
     char c = getchar();
     while(!isupper(c))
         c = getchar();
     return c;
 }

 int main(){
 #ifndef ONLINE_JUDGE
     freopen("2056.in" , "r" , stdin);
     //freopen("2056.out" , "w" , stdout);
 #endif
     N = read();
      ; i < N ; ++i){
         int a = read() , b = read();
         addEd(a , b);
         addEd(b , a);
     }
     init();
     for(int M = read() ; M ; --M)
         if(getc() == 'G')
             printf("%d\n" , query());
         else
             modify(read());
     ;
 }

捉迷藏

 #include<bits/stdc++.h>
 #define INF 0x3f3f3f3f
 //This code is written by Itst
 using namespace std;

 inline int read(){
     ;
     ;
     char c = getchar();
     while(c != EOF && !isdigit(c)){
         if(c == '-')
             f = ;
         c = getchar();
     }
     while(c != EOF && isdigit(c)){
         a = (a << ) + (a << ) + (c ^ ');
         c = getchar();
     }
     return f ? -a : a;
 }

 ;
 struct Edge{
     int end , upEd , w;
 }Ed[MAXN << ];
 ] , dis[MAXN][] , dep[MAXN] , size[MAXN] , ST[][MAXN << ] , fir[MAXN] , logg2[MAXN << ] , l[MAXN];
 int nowSize , minSize , minInd , ts , N , cntEd;
 unsigned char vis[MAXN];
 struct pq{
     priority_queue < int > now , del;
     inline void maintain(){
         while(!del.empty() && del.top() == now.top()){
             del.pop();
             now.pop();
         }
     }
     inline void push(int x){
         now.push(x);
     }
     inline void pop(int x){
         del.push(x);
     }
     inline int top(){
         maintain();
         return now.empty() ? -INF : now.top();
     }
     inline int sec(){
         maintain();
         if(now.empty())
             return -INF;
         int t = now.top();
         now.pop();
         maintain();
         int p = now.empty() ? -INF : now.top();
         now.push(t);
         return p;
     }
 }ans , cur[MAXN] , ch[MAXN];

 inline void addEd(int a , int b , int c){
     Ed[++cntEd].end = b;
     Ed[cntEd].upEd = head[a];
     Ed[cntEd].w = c;
     head[a] = cntEd;
 }

 void init_dfs(int now , int fa , int len){
     fir[now] = ++ts;
     ST[][ts] = now;
     dep[now] = dep[fa] + ;
     l[now] = len;
     for(int i = head[now] ; i ; i = Ed[i].upEd)
         if(Ed[i].end != fa){
             init_dfs(Ed[i].end , now , len + Ed[i].w);
             ST[][++ts] = now;
         }
 }

 inline int cmp(int a , int b){
     return dep[a] < dep[b] ? a : b;
 }

 inline void init_st(){
     logg2[] = -;
      ; i <= N <<  ; ++i)
         logg2[i] = logg2[i >> ] + ;
      ;  << i <= N <<  ; ++i)
          ; j + ( << i) -  <= N <<  ; ++j)
             ST[i][j] = cmp(ST[i - ][j] , ST[i - ][j + ( << (i - ))]);
 }

 inline int LCA(int x , int y){
     x = fir[x];
     y = fir[y];
     if(x < y)
         swap(x , y);
     ];
      << t) + ]);
 }

 void getSize(int x){
     vis[x] = ;
     ++nowSize;
     for(int i = head[x] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])
             getSize(Ed[i].end);
     vis[x] = ;
 }

 void getRoot(int x){
     vis[x] = size[x] = ;
     ;
     for(int i = head[x] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end]){
             getRoot(Ed[i].end);
             maxN = max(maxN , size[Ed[i].end]);
             size[x] += size[Ed[i].end];
         }
     maxN = max(maxN , nowSize - size[x]);
     if(maxN < minSize){
         minSize = maxN;
         minInd = x;
     }
     vis[x] = ;
 }

 inline int getLen(int x , int y){
     );
 }

 int init_dfz(int x , int pre){
     nowSize = ;
     minSize = INF;
     getSize(x);
     getRoot(x);
     x = minInd;
     vis[x] = ;
     fa[x][] = pre;
      , p = x ; fa[x][i] ; p = fa[x][i++]){
         dis[x][i] = getLen(x , fa[x][i]);
         fa[x][i + ] = fa[fa[x][i]][];
         ch[p].push(dis[x][i]);
     }
     for(int i = head[x] ; i ; i = Ed[i].upEd)
         if(!vis[Ed[i].end])
             cur[x].push(ch[init_dfz(Ed[i].end , x)].top());
     cur[x].push();
     cur[x].push();
     ans.push(cur[x].top() + cur[x].sec());
     vis[x] = ;
     return x;
 }

 inline void init(){
     init_dfs( ,  , );
     init_st();
     init_dfz( , );
 }

 inline void modify(int x){
     vis[x] ^= ;
     if(vis[x]){
         ans.pop(cur[x].top() + cur[x].sec());
         cur[x].pop();
         cur[x].pop();
         ans.push(cur[x].top() + cur[x].sec());
         int p = x;
          ; fa[x][i] ; p = fa[x][i++]){
             ans.pop(cur[fa[x][i]].top() + cur[fa[x][i]].sec());
             cur[fa[x][i]].pop(ch[p].top());
             ch[p].pop(dis[x][i]);
             cur[fa[x][i]].push(ch[p].top());
             ans.push(cur[fa[x][i]].top() + cur[fa[x][i]].sec());
         }
     }
     else{
         ans.pop(cur[x].top() + cur[x].sec());
         cur[x].push();
         cur[x].push();
         ans.push(cur[x].top() + cur[x].sec());
         int p = x;
          ; fa[x][i] ; p = fa[x][i++]){
             ans.pop(cur[fa[x][i]].top() + cur[fa[x][i]].sec());
             cur[fa[x][i]].pop(ch[p].top());
             ch[p].push(dis[x][i]);
             cur[fa[x][i]].push(ch[p].top());
             ans.push(cur[fa[x][i]].top() + cur[fa[x][i]].sec());
         }
     }
 }

 inline void query(){
     if(ans.top() <= -INF)
         puts("They have disappeared.");
     else
         printf("%d\n" , ans.top());
 }

 inline char getc(){
     char c = getchar();
     while(!isupper(c))
         c = getchar();
     return c;
 }

 int main(){
     #ifndef ONLINE_JUDGE
     freopen("4115.in" , "r" , stdin);
     freopen("4115.out" , "w" , stdout);
     #endif
     N = read();
      ; i < N ; ++i){
         int a = read() , b = read() , c = read();
         addEd(a , b , c);
         addEd(b , a , c);
     }
     init();
     for(int M = read() ; M ; --M)
         if(getc() == 'A')
             query();
         else
             modify(read());
     ;
 }

Qtree4

可以发现我们在上面维护了一个当前节点到它的父亲的一个堆,实际上这一种维护父亲的思想在点分树上经常会发挥很大作用,下一道题中我们也需要用到它。

点分树不仅擅长静态点分治擅长的路径拼接的操作,还擅长静态树上的动态换根统计

看下面这道题:幻想乡战略游戏

这道题简单来讲就是要求树上的带权重心,需要支持修改某个点的权值。

我们先来考虑每一次操作过后通过移动来维护带权重心。我们考虑将当前重心从一个点$x$移到它的一个儿子$y$上。我们不妨设$sum_i$表示$i$所在子树的权值和,那么我们移动产生的代价就是$delta = (sum_{root} - sum_y - sum_y) \times dis(x , y)$

那么如果说$sum_y \times 2 > sum_{root}$,意味着$delta < 0$,也就是说移动之后会更优。而显然对于某一个节点$x$来说,这样子的儿子$y$至多只有一个,如果没有就表示$x$是最优的答案了。

也就是说在每一次询问中,我们实际上可以任选一个点,然后通过$sum$的限制将重心移动,直到移动到最优的地方。

但是如果暴力去做的话,复杂度最坏是$O(n)$的。我们可以使用点分治的思想(上面练习题中“邮递员”一题就使用了这个思想),每一次如果找到一个最优的方向,就跳到对应子树的重心再去做,这样子就是$O(logn)$了。这一种操作就相当于在点分树上寻找一条路径。

然而在动态修改权的时候$sum$并不好维护,我们考虑维护以某一个点为根时的答案,比较两个点之间的答案大小来判断是否移动,实质和上面的分析是一样的。

我们对于点分树上每一个点,维护这三个东西:

$sumD:$当前点对应的分治范围的权值和

$sumV:$当前点对应的分治范围到达分治中心的代价总和

$upV:$当前点对应的分治范围到达分治中心的父亲的代价总和(维护父亲思想体现!)

然后考虑如何维护一个点的答案,直接看代码和注释吧

int sum = sumV[x] , p = x;
//答案初始化为所有子树的答案while(fa[x]){
    sum += sumV[fa[x]] - upV[x];    //祖先的其他子树到达这个祖先的答案
    sum += (sumD[fa[x]] - sumD[x]) * calcLen(p , fa[x]);
    //祖先的其他子树从祖先走到当前节点额外需要经过的路程,其中calcLen(x,y)表示计算x和y之间的距离    x = fa[x];
}
return sum;

这样我们只需要维护$sumV,upV,sumD$就能维护某一个点的答案了。这三个按照套路在每一次修改时暴跳点分树上父亲即可。

那么我们的$query$就可以这样进行:首先选择点分树上的根作为重心,$for$其点分树上的儿子,计算这个儿子所在子树的根的答案(注意不是这个儿子的答案),如果比当前更小就转移到点分树上的这个儿子,递归处理。

// luogu-judger-enable-o2
#include<bits/stdc++.h>
#define INF 0x7fffffff
#define int long long
//This code is written by Itst
using namespace std;

inline int read(){
    ;
    ;
    char c = getchar();
    while(c != EOF && !isdigit(c)){
        if(c == '-')
            f = ;
        c = getchar();
    }
    while(c != EOF && isdigit(c)){
        a = (a << ) + (a << ) + (c ^ ');
        c = getchar();
    }
    return f ? -a : a;
}

;
struct Edge{
    int end , upEd , len;
}Ed[MAXN << ];
int head[MAXN] , fa[MAXN] , size[MAXN] , sumD[MAXN] , sumV[MAXN] , upV[MAXN] , up[MAXN];
][MAXN << ] , fir[MAXN] , logg2[MAXN << ] , len[MAXN] , dep[MAXN];
int N , M , cntEd , nowSize , minSize , minInd , ts , root;
vector < int > ch[MAXN];
bool vis[MAXN];

inline void addEd(int a , int b , int c){
    Ed[++cntEd].end = b;
    Ed[cntEd].upEd = head[a];
    Ed[cntEd].len = c;
    head[a] = cntEd;
}

void getSize(int x){
    vis[x] = ;
    ++nowSize;
    for(int i = head[x] ; i ; i = Ed[i].upEd)
        if(!vis[Ed[i].end])
            getSize(Ed[i].end);
    vis[x] = ;
}

void getRoot(int x){
    size[x] = vis[x] = ;
    ;
    for(int i = head[x] ; i ; i = Ed[i].upEd)
        if(!vis[Ed[i].end]){
            getRoot(Ed[i].end);
            size[x] += size[Ed[i].end];
            maxN = max(maxN , size[Ed[i].end]);
        }
    maxN = max(maxN , nowSize - size[x]);
    if(maxN < minSize){
        minSize = maxN;
        minInd = x;
    }
    vis[x] = ;
}

int init_dfz(int x , int f){
    int p = x;
    nowSize = ;
    minSize = INF;
    getSize(x);
    getRoot(x);
    x = minInd;
    up[x] = p;
    fa[x] = f;
    vis[x] = ;
    for(int i = head[x] ; i ; i = Ed[i].upEd)
        if(!vis[Ed[i].end])
            ch[x].push_back(init_dfz(Ed[i].end , x));
    vis[x] = ;
    return x;
}

void init_dfs(int x , int p , int l){
    dep[x] = dep[p] + ;
    ST[][++ts] = x;
    fir[x] = ts;
    len[x] = l;
    for(int i = head[x] ; i ; i = Ed[i].upEd)
        if(!dep[Ed[i].end]){
            init_dfs(Ed[i].end , x , l + Ed[i].len);
            ST[][++ts] = x;
        }
}

inline int cmp(int x , int y){
    return dep[x] < dep[y] ? x : y;
}

void init_st(){
     ; i <= N <<  ; ++i)
        logg2[i] = logg2[i >> ] + ;
     ;  << i <= N <<  ; ++i)
         ; j + ( << i) <= N <<  ; ++j)
            ST[i][j] = cmp(ST[i - ][j] , ST[i - ][j + ( << (i - ))]);
}

void init(){
    root = init_dfz( , );
    init_dfs( ,  , );
    init_st();
}

inline int LCA(int x , int y){
    x = fir[x];
    y = fir[y];
    if(y < x)
        swap(x , y);
    ];
     << t) + ]);
}

inline int calcLen(int x , int y){
    );
}

inline int calc(int x){
    int sum = sumV[x] , p = x;
    while(fa[x]){
        sum += sumV[fa[x]] - upV[x];
        sum += (sumD[fa[x]] - sumD[x]) * calcLen(p , fa[x]);
        x = fa[x];
    }
    return sum;
}

int query(int x){
    int nowans = calc(x);
     ; i < ch[x].size() ; ++i)
        if(calc(up[ch[x][i]]) < nowans)
            return query(ch[x][i]);
    return nowans;
}

inline int modify(int x , int num){
    sumD[x] += num;
    int p = x;
    while(fa[x]){
        sumD[fa[x]] += num;
        upV[x] += num * calcLen(p , fa[x]);
        sumV[fa[x]] += num * calcLen(p , fa[x]);
        x = fa[x];
    }
    return query(root);
}

signed main(){
#ifndef ONLINE_JUDGE
    freopen("3345.in" , "r" , stdin);
    //freopen("3345.out" , "w" , stdout);
#endif
    N = read();
    M = read();
     ; i < N ; ++i){
        int a = read() , b = read() , c = read();
        addEd(a , b , c);
        addEd(b , a , c);
    }
    init();
     ; i <= M ; ++i){
        int x = read() , y = read();
        printf("%lld\n" , modify(x , y));
    }
    ;
}

幻想乡战略游戏

这道题可能不是很好理解。

最后放几道练习题:(Solution更新中)

Qtree5 Sol

开店 Sol

震波

烁烁的游戏

小清新数据结构题 Sol

紫荆花之恋

C.总结

点分治是一种通过选择树的重心,将原树剖成若干子树递归处理的一种树分治,擅长路径统计,而将点分治的遍历顺序建成树,就成为了点分树,因为点分树有高度不超过$log$的性质,所以可以在点分树上暴跳维护动态点分治,不仅可以处理路径统计,还可以处理换根统计问题,是一种很板子又很灵活的算法。

其实就是把前面的话抄了一遍