一道结论题。
直接给出结论,请大家思考为什么是这样的:
三个点两两的 L C A LCA LCA中,显然会有两个相同,另一个就是答案。
Subtask 1: n≤40
直接找到该树的直径,然后枚举核的两个端点,使其距离不超过 k k k;接着枚举所有点并求出它们与树核的距离的最大值即为偏心距。
注意预处理出节点两两之间的距离。
时间复杂度: O ( n 4 ) O(n^4) O(n4)。
Subtask 2: n≤200
可以发现,直径贯穿了整棵树;即,直径把树分为了许多块。
于是,我们枚举核的两个端点,使其距离不超过 s s s,然后从核上的每个端点向外进行 d f s dfs dfs,求得每个节点到核的距离,然后再找出最大值即可。
注意,我们确定了两个端点之后,将整棵树上的每个节点都不重复, 不遗漏地 d f s dfs dfs了一遍,故时间复杂度为 O ( n 3 ) O(n^3) O(n3)。
Subtask 3: n≤3000
容易发现,若核的长度变大(即,把原来的核在直径上延长),那么答案就会变小或不变。于是,我们可以在确定核的左端点为 l l l时,将右端点 r r r应尽量定在 l l l的远处,并且使其距离不超过 s s s。
于是,我们只需要枚举左端点,在 O ( n ) O(n) O(n)的代价下找到右端点 r r r;再用 S u b t a s k 2 Subtask 2 Subtask2中所述的方法找到偏心距即可。
时间复杂度: O ( n 2 ) O(n^2) O(n2)。
Subtask 4: n≤100,000
即,若一棵树的直径左端点为 a a a,右端点为 b b b,且对于核的左端点为 x x x,右端点为 y y y的核,其偏心距为 m a x ( m a x ( d i ) , d i s ( x , a ) , d i s ( y , b ) ) max(max(d_i),dis(x,a),dis(y,b)) max(max(di),dis(x,a),dis(y,b)),其中 i i i必须是被选择的节点编号。
同时,根据直径的定义,我们可以去掉"其中 i i i必须是被选择的节点编号"这句话。
总结一下本题的步骤:
①通过两次 d f s dfs dfs求出树的直径;
②对直径上的每一个点向外深搜,得到 d d d数组;
③找出 d d d数组中的最大值,记为 m a x v maxv maxv;
④通过双指针做法,枚举 i i i,找到最远且 i , j i,j i,j距离不超过 s s s的 j j j;每次用 m a x ( m a x ( d i s ( i , a ) , d i s ( j , b ) ) , m a x v ) max(max(dis(i,a),dis(j,b)),maxv) max(max(dis(i,a),dis(j,b)),maxv)来更新答案。
综上所述,时间复杂度为 O ( n ) O(n) O(n)。
首先,我们思考暴力的解法:我们枚举点 i i i,枚举 i i i的所有连点并打上时间戳;然后再次枚举连点,对于每个连点 u u u数一下存在多少个 u u u的连点被打上了 i i i的时间戳。
显然,这个时间复杂度会退化到 O ( n m ) O(nm) O(nm)。考虑优化。
我们先给每条边一个方向。即,度数小的连向度数大的边;若度数相同,则编号小的边连向编号大的边。
这样显然新图中不存在环。接着,我们同样用上述暴力来计数。
时间复杂度仍然是 O ( n m ) O(nm) O(nm),本题宣布不可做
但是,时间复杂度真的是 O ( n m ) O(nm) O(nm)吗?
考虑,对于新图中一条有向边 ( u , v ) (u,v) (u,v)对答案的贡献为 o u t v out_v outv,即 v v v的出度。
①原图中 v v v的出度不大于 m \sqrt m m,此时出度也不会大于 m \sqrt m m
②原图中 v v v的出度大于 m \sqrt m m,此时出度不会大于 m \sqrt m m。由于所有边的出度之和一定为 m m m,而不比该点出度小的连点一定不超过 m \sqrt m m个,所以新图中的出度不会大于 m \sqrt m m。
综上所述,时间复杂度为 O ( m m ) O(m \sqrt m) O(mm)。同时,新图中由于不存在环,所以不会出现一个环被重复计算的情况。
根号分治太可爱啦!
考虑只有一次询问,我们该怎么做。
首先,我们可以通过并查集求出询问的节点编号 u , v u,v u,v 所在的树。记 u u u 所在的树为树 1 1 1且树 1 1 1的大小为 n n n,记 v v v 所在的树为树 2 2 2 且树 2 2 2 的大小为 m m m, f i , j f_{i,j} fi,j 表示在第 i i i 棵树中,从第 j j j 个节点出发的最长链, d i a i dia_i diai 表示第 i i i棵树的直径。
此时答案为
∑ i = 1 n ∑ j = 1 m m a x ( f 1 , i + f 2 , j + 1 , d i a 1 , d i a 2 ) \sum_{i=1}^n \sum_{j=1}^m max(f_{1,i}+f_{2,j}+1,dia_1,dia_2) ∑i=1n∑j=1mmax(f1,i+f2,j+1,dia1,dia2)
这个式子的意义在于: 如果我们连了一条 i i i 到 j j j 的边使两棵树连起来,那么新树的直径要么是原来两棵树的直径之一,要么就是在树 1 1 1 中某个节点到 i i i 的最长距离,然后从 i i i 到 j j j ,再在树 2 2 2 中从 j j j 到别的节点的最长距离,即 f 1 , i + f 2 , j + 1 f_{1,i}+f_{2,j}+1 f1,i+f2,j+1 。
时间复杂度为 O ( n m ) O(nm) O(nm) ,考虑优化。可以发现,我们可以将 f 1 , i f_{1,i} f1,i 和 f 2 , i f_{2,i} f2,i 排序(即对于两棵树中,每棵树的每个节点出发的最长链长度从小到大排序),然后用双指针与前缀和做到 O ( n ) O(n) O(n) 。
更详细地说,对于每一个 i i i ,我们找到最靠前的 j j j 使得 f 1 , i + f 2 , j + 1 < m a x ( d i a 1 , d i a 2 ) f_{1,i}+f_{2,j}+1
不妨设 n < m n
对于多次询问,如果我们简单地使用上述方法,则时间复杂度最劣情况为为 O ( q n ) O(qn) O(qn) ,无法通过本题。
于是考虑对答案进行记忆化。即,对于本质相同的几次询问,我们只跑一次,对于之后的询问直接查询(不用再跑一遍)即可。
为什么说记忆化的时间复杂度是正确的呢?可以发现,在朴素的解法中,对时间复杂度贡献最大的就是两棵树大小均不小于 n \sqrt n n以上的情况。可以发现,当两棵树大小均不小于 k k k时,最多会产生 n 2 k 2 \frac {n^2} {k^2} k2n2种不同的树;同时,每跑一边会有 O ( k l o g n ) O(k logn) O(klogn)的时间复杂度,所以此时时间复杂度为 O ( n 2 k l o g n ) O(\frac {n^2} k logn) O(kn2logn)。当 k ≥ n k≥\sqrt n k≥n时,上述的值不会超过 O ( n n l o g n ) O(n \sqrt n logn) O(nnlogn);当 k < n k<\sqrt n k<n时,记忆化并没有多大用处,暴力即可把时间复杂度控制在 O ( n n l o g n ) O(n \sqrt n logn) O(nnlogn)。
综上所述,时间复杂度为 O ( n n l o g n ) O(n \sqrt n logn) O(nnlogn)。请相信 C F CF CF的神机是能跑过的。
我不会告诉你我这题调了 3h
由于 ∑ i = 1 n f i = f i + 2 − 1 \sum_{i=1}^n f_i=f_{i+2}-1 ∑i=1nfi=fi+2−1,所以
∑ i = 1 n i f i \sum_{i=1}^n if_i ∑i=1nifi
= ∑ i = 1 n ∑ j = i n f j =\sum_{i=1}^n \sum_{j=i}^n f_j =∑i=1n∑j=infj
= ∑ i = 1 n ( ∑ j = 1 n f j − ∑ j = 1 i − 1 f j ) =\sum_{i=1}^n (\sum_{j=1}^n f_j-\sum_{j=1}^{i-1} f_j) =∑i=1n(∑j=1nfj−∑j=1i−1fj)
= ∑ i = 1 n ( f n + 2 − 1 − ( f i + 1 − 1 ) ) =\sum_{i=1}^n (f_{n+2}-1-(f_{i+1}-1)) =∑i=1n(fn+2−1−(fi+1−1))
= ∑ i = 1 n ( f n + 2 − f i + 1 ) =\sum_{i=1}^n (f_{n+2}-f_{i+1}) =∑i=1n(fn+2−fi+1)
= n f n + 2 − ∑ i = 1 n f i + 1 =nf_{n+2}-\sum_{i=1}^n f_{i+1} =nfn+2−∑i=1nfi+1
= n f n + 2 − ( ∑ i = 1 n + 1 f i − f 1 ) =nf_{n+2}-(\sum_{i=1}^{n+1} f_i-f_1) =nfn+2−(∑i=1n+1fi−f1)
= n f n + 2 − ( f n + 3 − 1 − 1 ) =nf_{n+2}-(f_{n+3}-1-1) =nfn+2−(fn+3−1−1)
= n f n + 2 − f n + 3 + 2 =nf_{n+2}-f_{n+3}+2 =nfn+2−fn+3+2
直接用矩阵快速幂优化即可。注意设计减法,故要先取模再加模再取模。
时间复杂度 O ( l o g n ) O(logn) O(logn)。
一道裸的矩阵快速幂,但是涉及到一个优化的套路。
首先,我们通过无向图构造矩阵,对于每次询问直接做矩阵快速幂,时间复杂度为 O ( q n 3 l o g f i ) O(qn^3log{f_i}) O(qn3logfi)。期望得分 40 40 40分。
我们考虑如何将其优化为 O ( q n 2 l o g f i ) O(qn^2log{f_i}) O(qn2logfi)。我们可以发现,像 NOI D1T1 \color {red} \text {NOI D1T1} NOI D1T1一样,可以通过向量乘矩阵来优化。即,我们将 f i f_i fi进行二进制拆分,倍增预处理;查询的时候,用 1 × n 1×n 1×n的矩阵乘上对应的二进制位即可。
时间复杂度 O ( l o g f i n 3 + q n 2 l o g f i ) O(log{f_i}n^3+qn^2log{f_i}) O(logfin3+qn2logfi),可以通过本题。
所以大家今晚就把NOI 2020的题切了吧
考虑矩阵快速幂。
首先,假设所有边权都是 1 1 1,那么本题做法就十分显然了:直接将图转化为一个邻接矩阵(GA),然后答案就是该矩阵 t t t次方的第 1 1 1行第 n n n列。
但是如果边权不一定全是 1 1 1呢?那我们就把边权变成 1 1 1呗。即,我们拆点,假设存在一个点 a a a,那么我们新建节点 b , c , d … … b,c,d…… b,c,d……,使得 b b b到 a a a有一条长度为 1 1 1的有向边, c c c到 b b b也有一条长度为 1 1 1的有向边……
对于一条连接 u , v u,v u,v的边权为 w w w,我们只需要从 u u u到离 v v v距离为 w − 1 w-1 w−1的点连一条边权为 1 1 1的边。
此时,点数为 10 n 10n 10n,我们直接对它做矩阵快速幂即可通过本题。
时间复杂度 O ( l o g t ( 10 n ) 3 ) O(logt(10n)^3) O(logt(10n)3)。
首先,考虑如何拿到那 30 30 30分。不妨设 m ≤ 5 m≤5 m≤5,此时可以看出,这是一个裸的三进制状态 d p dp dp。我们维护每一列的状况,其中第 i i i位表示该位被填了多少个炮,显然所有状态压缩的值的各位均不能达到 3 3 3,所以采用三进制状压 d p dp dp。时间复杂度 O ( n 2 3 m ) O(n^2 3^m) O(n23m)。
接着考虑正解。可以发现,我们不关心哪一列填了多少个炮,只关心这个状态的本质;换句话说,状态 10121 10121 10121与 20111 20111 20111是等价的。
所以,我们直接采用高效地四维 d p dp dp解决本题。状态设计显然: d p i , a , b , c dp_{i,a,b,c} dpi,a,b,c表示第看到第 i i i行,有 a a a列没填炮, b b b列
填了一个炮, c c c列填了两个炮,然后每行分别考虑填 0 , 1 , 2 0,1,2 0,1,2个的情况,分类讨论即可得到状态转移式。显然状态转移是 O ( 1 ) O(1) O(1)的,故总时间复杂度为 O ( n m 3 ) O(nm^3) O(nm3)。
但是,这样做只有 50 50 50分。我们观察状态设计,可以发现, a + b + c = m a+b+c=m a+b+c=m;即 c = m − a − b c=m-a-b c=m−a−b, c c c是可以通过 a , b a,b a,b推出唯一的可能的,故可以压成三维 d p dp dp: d p i , a , b dp_{i,a,b} dpi,a,b表示第看到第 i i i行, a a a列填了一个炮, b b b列填了两个炮, m − a − b m-a-b m−a−b列没填炮。
状态转移如下:
for (int i=1;i<=n;i++)
{
for (int j=0;j<=m;j++)
{
for (int k=0;k<=m-j;k++)
{
int w=m-j-k;
dp[i][j][k]=dp[i-1][j][k];
if (j>0) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-1][k]*(w+1))%mod;
if (k>0) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j+1][k-1]*(j+1))%mod;
if (j>1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-2][k]*C2(w+2))%mod;
if (j>0&&k>0) dp[i][j][k]=(dp[i][j][k]+(dp[i-1][j][k-1]*j)%mod*(w+1))%mod;
if (k>1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j+2][k-2]*C2(j+2))%mod;
}
}
}
时间复杂度 O ( n m 2 ) O(nm^2) O(nm2),可以通过本题。
首先,可以发现,假设该树直径的两个端点为 u , v u,v u,v,则 d ( x ) = m a x ( d i s ( x , u ) , d i s ( x , v ) ) d(x)=max(dis(x,u),dis(x,v)) d(x)=max(dis(x,u),dis(x,v))。所以,我们先求出 u , v u,v u,v,然后即可在 O ( n l o g n ) O(nlogn) O(nlogn)的时间复杂度内求出每个节点的 d d d值。
我们可以发现,若以直径的中点为根,则整棵树会形成一个这样的结构:每个节点的 d d d值均不大于它子树中各节点的 d d d值。
对于每次询问,我们考虑双指针。即,我们对所有节点按 d d d值的升序排序,在 i i i向前的时候,维护最大的 j j j使 d j − d i ≤ l d_j-d_i≤l dj−di≤l,显然 j j j会单调下降。我们同时用按秩合并并查集来维护当前区间中的最大连通块。
但是,并查集不能支持删除操作。但是,由于这棵树的性质(每个节点的 d d d值均不大于它子树中各节点的 d d d值),所以删去的节点一定是叶节点;只需要在叶节点的对应祖先那里扣除相应的值即可,不会影响连通性。
时间复杂度 O ( n l o g n + q n ) O(nlogn+qn) O(nlogn+qn)。注意并查集强制按秩合并,否则无法通过。
如果不会做,找个人与你博弈一下。
首先,我们找到树的直径,将两人一次能够走动的距离对这个直径取 m i n min min。
然后,只要出现以下两种情况,那么 A l i c e Alice Alice就能抓到 B o b Bob Bob:
① d b ≤ 2 d a db≤2da db≤2da
② d i s ( a , b ) ≤ d a dis(a,b)≤da dis(a,b)≤da
①为什么满足呢?因为,当 B o b Bob Bob被逼到绝路的时候, A l i c e Alice Alice与他的距离为 d b db db;假设 d a = 2 d b da=2db da=2db,那么 B o b Bob Bob只能向前跳,尝试越过 A l i c e Alice Alice,可是一切都是徒劳,跳完之后还是与她距离为 d b db db,就会被抓住了。
②一次就能抓住,样例有提示。
其他情况都抓不到。
于是时间复杂度为 O ( n ) O(n) O(n)。
为了方便思考,我们把题目中的一操作变成:
⌊ \lfloor ⌊在以 u u u为根的子树中,将所有深度为 x x x的节点的点权 w w w加 1 1 1。 ⌋ \rfloor ⌋
现在,这变成了一个多点修改,多点查询的模型,但是唯一的问题在于——每次我们操作、询问的节点编号不一定连续,所以线段树很难维护。
那么,我们要让这些节点的编号连续。怎么办呢?
众所周知,我们有一个东西叫做 d f s dfs dfs序;本题中采用的就是她并不是太有名的妹妹,即 b f s bfs bfs序。
b f s bfs bfs序有什么好处呢?可以发现,每次我们操作的都是一段连续的区间。但是,我们如何找到该区间的左端点与右端点呢?
我们使用二分套倍增来找,这里以找左端点为例。首先,左端点与右端点所在的深度(这里的深度是指整棵树中节点的深度)是很容易求出来的。对于我们二分到的一个节点,我们通过倍增求出它的一个与 u u u平齐的祖先。如果这个祖先的 b f s bfs bfs序比 u u u的 b f s bfs bfs序小,那么它就偏左了;否则它就偏右了。通过这样的方式,我们可以在不超过 O ( l o g 2 n ) O(log^2n) O(log2n)的复杂度内找到左端点,右端点同理。
找到左端点与右端点之后,这道题目就变成了一个裸的区间修改与区间查询,用线段树可以轻松维护。
但是,要注意的是: 我们刚开始改了题目,原题中不是区间加法,而是区间中的所有数变成其约数的个数!
此时,各位巨佬的脑中已经有了一个重要的套路——由于这个数不会超过 1 0 7 10^7 107,所以通过最多 7 7 7次操作就会变成 2 2 2! 如果这个数是 1 1 1,它就永远是 1 1 1!
与CF920F类似,我们使用线段树就可以维护。线段树中维护两个量: 该区间的最大值与和。如果最大值不超过 2 2 2,那么我们就再也不进入这个节点和其子树;否则就正常维护线段树,甚至连下传标记都不需要。
最后剩下的一个问题,就是如何求出 f f f。
①我们枚举 i i i,对于所有是 i i i的倍数的 j j j均加上 1 1 1,表示 j j j多了一个约数 i i i。
②可以发现, f f f是积性函数。
即,若 x x x与 y y y互质,那么 f ( x ) f ( y ) = f ( x y ) f(x)f(y)=f(xy) f(x)f(y)=f(xy)。
所以,我们使用欧拉线性筛即可求出 f f f。时间复杂度 O ( m a x 1 ≤ i ≤ n w i ) O(max_{1≤i≤n} w_i) O(max1≤i≤nwi)。
所以,本题就做完了。总结一下:
①使用欧拉线性筛求出 f f f,使后来可以 O ( 1 ) O(1) O(1)调用。
②读入树后找出它的 b f s bfs bfs序,并建线段树;
③对于每次询问和操作,通过二分套倍增找到被操作的左端点与右端点;
④用特殊的线段树进行区间修改或区间查询。
时间复杂度 O ( t + n + q l o g 2 n ) O(t+n+qlog^2n) O(t+n+qlog2n),其中 t = m a x 1 ≤ i ≤ n w i t=max_{1≤i≤n} w_i t=max1≤i≤nwi
大神仙题
首先,我们先干这两件事情:
①我们套路性地化边为点;
②将每个权值给二进制拆分,尝试维护每一位的答案。
对于这一类神仙题,我们先把它转到一条链上去做。为了再进一步简化问题,我们把所有修改去掉,假设只有查询。
现在,相当于 q q q次查询一个区间的所有子区间的异或和之和。怎么做呢?我们考虑线段树。线段树上每个节点要维护一个答案,这是显然的;但是,这里线段树上一个节点的答案,不仅是它两个孩子的答案,还有跨过两个子区间的答案。
对于"它两个孩子的答案之和",直接上传即可。
对于"跨过两个子区间的答案”呢?我们分析一下这种区间的性质,它一定包含了左区间的一个后缀与右区间的一个前缀;如果左区间的后缀异或和与右区间的前缀异或和不同,那么就存在一个 1 1 1的贡献。所以我们可以另外使用四个变量,来方便上传。
p r e 0 pre0 pre0: 这个区间有多少个前缀的异或和为 0 0 0。
p r e 1 pre1 pre1: 这个区间有多少个前缀的异或和为 1 1 1。
s u f 0 suf0 suf0: 这个区间有多少个后缀的异或和为 0 0 0。
s u f 1 suf1 suf1: 这个区间有多少个后缀的异或和为 1 1 1。
写出 a n s ans ans的上传式,就是 a n s = l s o n a n s + r s o n a n s + l s o n s u f 0 r s o n p r e 1 + l s o n s u f 1 r s o n p r e 0 ans=lson_{ans}+rson_{ans}+lson_{suf0}rson_{pre1}+lson_{suf1}rson_{pre0} ans=lsonans+rsonans+lsonsuf0rsonpre1+lsonsuf1rsonpre0
现在,考虑如何上传 p r e 0 , p r e 1 , s u f 0 , s u f 1 pre0,pre1,suf0,suf1 pre0,pre1,suf0,suf1。以 p r e 0 pre0 pre0举例,左孩子的所有异或和为 0 0 0的前缀仍然有贡献,但是右区间呢?右区间中一些异或和为 0 0 0的前缀还要异或上左区间所有数,才能使得它成为它父区间的前缀;而这可能会导致其异或值为 1 1 1而失去它的价值。
于是,我们在线段树中再开一个变量 a l l all all,表示这个区间所有数的异或和。那么,我们可以愉快地上传啦——仍然以 p r e 0 pre0 pre0为例:
p r e 0 = l s o n p r e 0 + ( l s o n a l l ? ( r s o n p r e 1 ) : ( r s o n p r e 0 ) ) pre0=lson_{pre0}+(lson_{all}?(rson_{pre1}):(rson_{pre0})) pre0=lsonpre0+(lsonall?(rsonpre1):(rsonpre0))
什么意思呢?如果左区间所有数的异或和为 1 1 1,只有右区间的一个前缀和为 1 1 1才能使得新形成的前缀和异或和为 0 0 0;而如果左区间所有数的异或和为 0 0 0,右区间的一个前缀和为 0 0 0才可以。所以得到了这个上传式。
a l l all all上传的方式不能再显然了: a l l = l s o n a l l all=lson_{all} all=lsonall^ r s o n a l l rson_{all} rsonall,这里的^表示异或。
综上所述,状态转移式就是:
now.all=lson.all^rson.all;
now.ans=lson.ans+rson.ans+(lson.suf0*rson.pre1)+(lson.suf1*rson.pre0);
now.pre0=lson.pre0+((lson.all)?(rson.pre1):(rson.pre0));
now.pre1=lson.pre1+((lson.all)?(rson.pre0):(rson.pre1));
now.suf0=rson.suf0+((rson.all)?(lson.suf1):(lson.suf0));
now.suf1=rson.suf1+((rson.all)?(lson.suf0):(lson.suf1));
众所周知,在线段树维护不带修改问题这一方面,我们只需要搞出 p u s h u p pushup pushup函数即可。于是,我们就搞定了不带修改的链的问题。
带上单点修改呢?我们直接下到线段树的叶节点,然后一步步向上走,一边走一边 p u s h u p pushup pushup更新当前节点的各个信息不就行了。
不是链了,而是树了,怎么办呢?树链剖分套线段树呗。
于是,我们就在 O ( n log 2 n log w i ) O(n\log^2n \log{w_i}) O(nlog2nlogwi)的时间复杂度内解决了本题。建议读者打一下这题的代码,毕竟本蒟蒻码了 309 309 309行,很练码力的……