一篇自己都看不懂的点分治&点分树学习笔记
淀粉质点分治可真是个好东西
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$
然后放几道练习题:
基础(比较裸就没有讲什么了):
#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; ; }
聪聪可可
#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更新中)
B.动态点分治(点分树)
什么?点分治还能带修改?Of course!
我们可以发现:根据点分治,我们可以构建出一棵树,在点分治过程中,如果从$solve(a)$递归到了$solve(b)$,就令$a$所在分治区域的重心为$b$所在分治区域重心的父亲,这样我们就可以构造出点分树。点分树有几个优美的性质:
$a.$点分树的高度不超过$O(logn)$,因为点分治的递归深度不会超过$logn$
$b.$点分树上某个点的祖先(包括它自己)在点分治时的分治范围必定包括了这个点,而其他点的分治范围一定不会包含这个点。
$c.$点分树上某个点的儿子一定在这一个点的分治范围的子树中(废话)
这个性质告诉我们:如果在点分树上进行修改,只需要修改它到根的一条链,修改点数不会多于$logn$。
我们就说$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更新中)
C.总结
点分治是一种通过选择树的重心,将原树剖成若干子树递归处理的一种树分治,擅长路径统计,而将点分治的遍历顺序建成树,就成为了点分树,因为点分树有高度不超过$log$的性质,所以可以在点分树上暴跳维护动态点分治,不仅可以处理路径统计,还可以处理换根统计问题,是一种很板子又很灵活的算法。
其实就是把前面的话抄了一遍
一篇自己都看不懂的点分治&点分树学习笔记的更多相关文章
- 一篇自己都看不懂的CDQ分治&整体二分学习笔记
作为一个永不咕咕咕的博主,我来更笔记辣qaq CDQ分治 CDQ分治的思想还是比较简单的.它的基本流程是: \(1.\)将所有修改操作和查询操作按照时间顺序并在一起,形成一段序列.显然,会影响查询操作 ...
- 一篇自己都看不懂的Matrix tree总结
Matrix tree定理用于连通图生成树计数,由于博主太菜看不懂定理证明,所以本篇博客不提供\(Matrix\ tree\)定理的证明内容(反正这个东西背结论就可以了是吧) 理解\(Matrix\ ...
- 对于挑战书上的很久之前都看不懂的DP看懂的突破
突破一..牢记问题概念 并且牢记dp状态方程 突破二..一直有一个求和dp转化成O1dp递推的式子看不懂.. 看不懂的原因是..没有分清求和符号作用的范围 提醒:以后遇到求和符号一定明确其求和的式子的 ...
- 一看就懂之吐血整理的 Docker 学习笔记
前言 随着devops的兴起,Docker 近年来越来越火,容器化概念也越来越火.此篇将带你瞬间入门Docker. Docker 概述 Docker 出现的背景 在我们的开发过程中,通常是这样的一种流 ...
- ASP.NET Core Web开发学习笔记-1介绍篇
ASP.NET Core Web开发学习笔记-1介绍篇 给大家说声报歉,从2012年个人情感破裂的那一天,本人的51CTO,CnBlogs,Csdn,QQ,Weboo就再也没有更新过.踏实的生活(曾辞 ...
- Python学习笔记基础篇——总览
Python初识与简介[开篇] Python学习笔记——基础篇[第一周]——变量与赋值.用户交互.条件判断.循环控制.数据类型.文本操作 Python学习笔记——基础篇[第二周]——解释器.字符串.列 ...
- async,await执行流看不懂?看完这篇以后再也不会了
昨天有朋友在公众号发消息说看不懂await,async执行流,其实看不懂太正常了,因为你没经过社会的毒打,没吃过牢饭就不知道自由有多重要,没生过病就不知道健康有多重要,没用过ContinueWith就 ...
- 还看不懂同事的代码?Lambda 表达式、函数接口了解一下
当前时间:2019年 11月 11日,距离 JDK 14 发布时间(2020年3月17日)还有多少天? // 距离JDK 14 发布还有多少天? LocalDate jdk14 = LocalDate ...
- 还看不懂同事的代码?超强的 Stream 流操作姿势还不学习一下
Java 8 新特性系列文章索引. Jdk14都要出了,还不能使用 Optional优雅的处理空指针? Jdk14 都要出了,Jdk8 的时间处理姿势还不了解一下? 还看不懂同事的代码?Lambda ...
随机推荐
- Spring学习之旅(四)Spring工作原理再探
上篇博文对Spring的工作原理做了个大概的介绍,想看的同学请出门左转.今天详细说几点. (一)Spring IoC容器及其实例化与使用 Spring IoC容器负责Bean的实例化.配置和组装工作有 ...
- Android滑动冲突解决
(1).场景一:外部滑动方向跟内部滑动方向不一致,比如外部左右滑动,内部上下滑动 ViewPager+Fragment配合使用,会有滑动冲突,但是ViewPager内部处理了这种滑动冲突 如果 ...
- WPF:完美自定义MeaagseBox 2.0
很久前做个一个MessageBox,原文链接:http://www.cnblogs.com/DoNetCoder/p/3843658.html. 不过对比MessageBox还有一些瑕疵.这些天有时间 ...
- 微信小程序-全国快递查询
微信小程序-全国快递查询 摘要:WeChat.小程序.JS 开发过程 源码下载 1. GitHub 2. 百度云 链接:https://pan.baidu.com/s/1XVbtT2JsZslg4Y0 ...
- (网页)websocket后台调用Service层
百度论坛里面有很多好的方法,借鉴. 重点:因为项目是StringBoot所以我用的是下面的方法很好使: Service.... service = (Service....) ContextLoade ...
- Nginx与Nginx-rtmp-module搭建RTMP视频直播和点播服务器
一.开发环境 Nginx地址: http://nginx.org/download/nginx-1.14.2.tar.gz Nginx-rtmp-module地址: https://github.c ...
- springmvc复习笔记----文件上传multipartResolver
结构 web.xml <?xml version="1.0" encoding=&q ...
- MapReduce ----数据去重
三个文件 2017-03-10 a2017-03-11 b2017-03-12 d2017-03-13 d2017-03-142017-03-15 a 2017-03-10 e2017-03-11 b ...
- emacs 绑定快捷键 c/c++
;;跳转到函数定义的地方 (global-set-key (kbd "C-<f2>") 'ggtags-find-definition) ; Control+F2 ;; ...
- win10系统中如何解决cmd中的路径和现在电脑的用户名不一致
假设原用户名老王,已删除,但是cmd后路径还是C:\Users\老王>,这与现在用户laowng不一致了需改为C:\Users\laowang>. .先新建一个管理员账户laowang,然 ...