专题练习——数据结构
P2056 [ZJOI2007] 捉迷藏
解析
首先距离问题+加修改,我们可以很容易的想到点分树,考虑怎么维护
经过一个重心的最大距离等于最大的两条不相交链之和,因此我们维护两个值,\(nmx_u\) 表示最长的不相交链,\(fmx_u\) 表示子树中到父亲的最长链(对于父亲来说也就是单个子树的最长链)
由于有修改,加上最后的答案,我们要用手写可删堆来维护 \(nmx,fmx,ans\)
至于修改,如果是熄灭就向堆中加 \(dis\),反之就减 \(dis\)
启发
点分树的维护:整个子树的数据,单个儿子构成的子树的数据(也就是子树中所有点到根节点父亲的数据)
手写可删堆
struct heap{
priority_queue<int> add,del;
int size(){return add.size()-del.size();}
void push(int x){add.push(x);}
void erase(int x){del.push(x);}
void pop(){
while(!del.empty()&&del.top()==add.top()) add.pop(),del.pop();
add.pop();
}
int top(){
while(!del.empty()&&del.top()==add.top()) add.pop(),del.pop();
return add.empty()?0:add.top();
}
int top2(){
if(size()<2) return 0;
int x=top();pop();
int y=top();push(x);
return y;
}
}
P8123 [BalticOI 2021] Inside information (Day1)
解析
12操作很简单,就是线段树合并,但由于合并完后又要两个点是独立的,因此要用主席树的合并
难点在于操作3,发现个数是满足一个反向关系的,也就是说,要想包含一个点 \(x\),必须要保证路径上 \(x\) 连出的边是最小的(如图)

那么我们可以想到暴力做法,也就是反向加边,算联通块大小
考虑优化,我们考虑加边的过程,也就是将两个联通块的siz加起来,那么我们可以以时间为坐标,维护每个时间点每个点联通块siz加的数,那么连边就相当于线段树合并,答案就是区间和+1(自身的大小),注意合并时也要+1
启发
处理不同时间的查询,可以以时间为坐标,那么问题也就转化为了区间和
P9233 [蓝桥杯 2023 省 A] 颜色平衡树
解析
板题,全部相同的条件相当于最大个数等于最小个数(注意不包含0),然后线段树合并即可
P5889 跳树
解析
显然操作是要合并的,但是我们发现难点在于我们不能简单的用一个数来合并,而要用多个数来维护
我们先考虑最基础的:
- 父亲:\((s>>1)\)
- 左儿子:\((s<<1)\)
- 右儿子:\(((s<<1)|1)\)
我们发现这三者都可以表示为 \((((s>>a)<<b)|c)\) 的形式,因此我们想到维护 \(a,b,c\),赋值是显然的
接下来考虑如何合并,其实也很简单,相当于先使 \(s \rarr (max(1,(s>>a_l)<<b_l)|c_l)\),然后再使 \(s \rarr (max(1,(s<<a_r)>>b_r)|c_r)\),可以得到合并方式:\(\begin{cases} a=a_l+max(a_r-b_l,0)\\ b=max(b_l-a_r,0)+b_r \\c=((c_l>>a_r)<<b_r)|c_r \end{cases}\)
剩下的就是普通线段树了
P6186 [NOI Online #1 提高组] 冒泡排序
解析
我们考虑每个数的贡献,就是所有在它之前且大于它的数,我们定义为 \(f_i\)。
考虑一次冒泡排序对答案的贡献,相当于这样的变化

那么也就相当于中间部分的 \(f\) 减一,而最右边的 \(f\) 已经为0所以不减,因此我们直接从 \(0 \sim n-1\) 遍历 \(f\) 的大小预处理即可
但是还有交换的情况,发现交换一次只有两个 \(f\) 的变化,因此我们将预处理拆开,这样就变成了单点修,区间求和,树状数组即可
启发
逆序对:从单个贡献入手
P5524 [Ynoi2012] NOIP2015 充满了希望
应该是最简单的Ynoi了
解析
由于是赋值操作,那么每个点的取值只有最后一个覆盖到它的赋值操作 \(lst\) 决定,就是区间修单点查
交换就是交换两个点的 \(lst\)
然后考虑求和,一个点要想被计算的条件是 \(l\le lst<t\le r\),(贪心显然),那么就是二维偏序,离散化查询即可
P3792 由乃与大母神原型和偶像崇拜
解析
用类似于哈希的做法,比较区间minmax,区间和,区间平方和是否于连续序列的相同即可
P6327 区间加区间 sin 和
解析
\(\begin{cases}\sin(a+x)=\sin(a)*\cos(x)+\cos(a)*\sin(x) \\ \cos(a+x)=\cos(a)*\cos(x)-\sin(a)*\sin(x)\end{cases}\),直接维护区间 \(\sin,\cos\) 和即可
P11620 [Ynoi Easy Round 2025] TEST_34
解析
首先区间求值是简单的,直接维护一颗线段树套线性基即可
然后我们考虑区间异或,由于直接加复杂度会超的,我们考虑维护差分数组 \(b_i=a_i \oplus a_{i-1}\),那么此时区间修就变为了单点修
又因为 \(a_i=a_l \oplus b_{l+1} \oplus ... \oplus b_i\),因此 \(a_l \sim a_r\) 的线性基就等价于 \(a_l\) 和 \(b_{l+1} \sim b_r\) 的线性基,那么求值又变为区间求
启发
- 异或最值:线性基
- 异或的区间处理:可以考虑差分后转化为单点处理
P2605 [ZJOI2010] 基站选址
解析
dp是显然的:\(dp_{i,j}=dp_{k,j-1}+cost(k,i)+c_i\),其中 \(cost(k,i)\) 指 \(k \sim i\) 中未被覆盖的 \(w\) 之和
显然要用线段树来维护 \(dp_{k,j-1}+cost(k,i)\)
考虑从 \(i-1 \rarr i\) 的变化: 设 \(st_i,ed_i\) 为能使 \(i\) 被覆盖的最远点,那么从 \(i-1 \sim i\) 时所有满足 \(ed_j=i-1\) 的 \(j\) 会使 \(1 \sim st_j-1\) 的 \(cost\) 加 \(w_j\),直接区间加即可
反思
想成了前缀和作差,没有想到直接维护 \(dp+cost\)
P4587 [FJOI2016] 神秘数
解析
假设现在可以算出 \(1 \sim x\),考虑加入一个数 \(v\),有两种情况:
- \(v\le x+1\),那么 \(v \sim v+x\) 都能被表示,值域变为 \(1 \sim x+v\)
- \(v>x\),那么 \(x+1\) 一定表示不出来
那么我们就可以想到一个暴力:从小到大加数直到不能加
我们发现一个一个是不必要的,比如说现在选的最大值为 \(lst\),能表示的最值为 \(now\),那么 \(lst+1 \sim now+1\) 的数都是可以选的,因此我们直接区间求和即可,又因为 \(\sum a_i \le 10^9\),因此总范围是小于 \(10^9\) 的,因此复杂度就是 \(O(n\log n \log(\sum a_i))\)
启发/反思
一直再想用选取方案数来判断是否能表示玩,没有想到先直接暴力的做
P4602 [CTSC2018] 混合果汁
终于做出来道紫的数据结构(喜)
解析
首先肯定二分答案,check也就是看用 \(d_i\) 在 \(mid \sim 10^5\) 的果汁能不能满足要求,用主席树就可以维护这个区间信息
考虑如何check,贪心的想,我们肯定是每次选单价最小的,因此我们就想到以单价为主席树的横坐标,维护体积,花费,然后再线段树上二分即可
P2839 [国家集训队] middle
解析
首先二分答案 \(mid\),然后一个显然的处理是将 \(\le mid\) 的取-1,\(>mid\) 的取1,然后看区间的和,这个可以主席树完成:先默认每个都为1,每次单点修改为-1
正常来讲 \(mid\) 合法的要求是满足区间和为0或1,但由于二分只是确定边界,因此只需要保证满足区间和大于等于0,就可以证明一定有比 \(\ge mid\) 的答案
求区间的最大和是简单的:\(a \sim b\) 的最大后缀+ \(b+1 \sim c-1\) 的区间和+ \(c\sim d\) 的前缀最大值
启发
二分的本质是利用单调性缩小上下界,因此check是确定答案能否在左区间或右区间(这个题就是个很好的例子,对于 \(i,j \ (i<j)\) 来说,可能 \(j\) 能做中位数,而 \(i\) 不能做,但 \(r\sim j\) 有合法中位数证明 \(r \sim i\) 有合法中位数,因此它也是有单调性的)
P2824 [HEOI2016/TJOI2016] 排序
思路
ODT,每个联通块代表一个升序/降序序列,每个联通块维护一个值域线段树,查询时直接查线段树第 \(k\) 大即可,区间排序与正常的ODT类似,只是加上了线段树合并和分裂
启发
P4770 [NOI2018] 你的名字
思路
Case 1:\(l=1,r=\lvert s \rvert\)
首先有一个显然的转化:不匹配的数量=总子串数-匹配的子串数
匹配子串数直接做是不好做的,我们考虑在后缀自动机上求解,定义 \(mx_{u}\) 为 \(u\) 节点所代表的子串中能匹配的最大长度,那么总匹配数就是 \(\sum mx_u-len_{fa_{u}}\)
接下来考虑如何更新,我们可以对于 \(T\) 的每个前缀求出能匹配的最长后缀,然后在parent树上跳父亲更新即可(本质就是将最长后缀给拆分了),同时我们每次跳父亲时,如果跳到 \(mx_u=len_u\) 的情况也就不用再跳了(已经最大),这样就可以保证复杂度
求前缀的最大匹配后缀,我们可以给 \(S\) 也开一个后缀自动机,由于SAM的parent树与ACAM的fail树性质类似,我们可以同样的在SAM跳父亲:从算完 \(1\sim i-1\) 后的节点 \(p\) 开始,跳父亲直到有 \(T_i\) 的儿子,最大后缀就是这个节点的 \(len+1\)
Case 2:\(l,r\) 任意
直接将 \(l\sim r\) 的SAM取出来是不现实的,但是我们可以利用原来的SAM达到这样的效果
考虑一个节点,它能在新SAM上存在,当且仅当它存在一个 \(endpos=p\),使得 \(l \le p \le r\),此时它的 \(len\) 变为 \(l\sim r\) 之间的最大长度(感性理解,相当于暴力去除 \(1 \sim l-1\) 和 \(r+1 \sim n\) 留下的直接作新节点)
那么我们就可以对于每个节点开一个线段树维护 \(endpos\) 集合,判断为 \(1 \sim r\) 的最大值是否 \(\ge l\),\(len=min(len,maxpos(1,r)-l+1)\)
线段树的求取直接在parent树上合并儿子即可,注意要持久化
P3332 [ZJOI2013] K 大数查询
思路
线段树套线段树,但是外层是值域线段树,内层是区间线段树
反思/启发
没有想到外层用值域线段树,线段树种类的选取是灵活的
P3242 [HNOI2015] 接水果
初始的想法是表示出包含关系,但是发现并不可行,因为表示一条路径要用 \(O(\log n)\) 个区间,复杂度不过关
我们想到能用一个区间表示的是子树关系,因此我们考虑表示被包含关系
如下图,分为两种情况
- \(lca(x,y)=root\):此时一点在 \(x\) 子树中,一点在 \(y\) 子树中
- \(lca(x,y) \neq root\):假设 \(dep_x<dep_y\),此时一点在 \(y\) 子树中,一点在 \(z\) 子树外(\(z\) 为 \(x \rarr y\) 上最近一点)
(\(z\) 处笔误了,应为 \((1,st_z-1) \lor (ed_z+1,n)\))
这时就变为了二维数点问题:求包含一个点的矩形中权值第 \(k\) 大的点,整体二分即可
反思/启发
- 正难则反,包含于被包含的转换
- 整体二分
P12685 [国家集训队] 排队 加强版
思路
动态逆序对板子,每次交换时减去 \(x,y\) 的贡献,交换后加上新的 \(x,y\) 的贡献即可,注意常数
P4093 [HEOI2016/TJOI2016] 序列
二维数点优化dp转移
设所有变化中的最大最小值为 \(mn_i,mx_i\),那么能从 \(i \rarr j\) 的条件是 \(a_j \ge mx_i,mn_j \ge a_i\),直接树套树维护二维区间的最大值即可
P5621 [DBOI2019] 德丽莎世界第一可爱
思路
四维偏序,考虑从三维偏序衍生,三维偏序的步骤:
- 将第一维排序
- 递归处理子问题,当前函数中处理 \([l,mid]\) 和 \([mid+1,r]\) 之间的偏序对
- 分别将 \([l,mid]\) 和 \([mid+1,r]\) 按第二维排序,这里虽然将第一维顺序打乱,但保证了 \(\forall i \in [l,mid],j \in [mid+1,r]\ , a_i \le a_j\),因此可以处理
- 用树状数组维护第三维
四维偏序中的后三维可以按照这个方式来处理,但问题在于第一维,我们考虑再套一层分治,像上面的 \(2,3\) 步一样,单次分治中只处理跨过 \(mid\) 的关系,这样我们只需要给每个数打上一个属于左边还是右边的标记,就可以维护第一维,复杂度 \(O(n \log^3 n)\)
启发
对cdq分治的进一步思考,怎么在第二维有序的情况维护第一维:分治解决,每次只保证 \([mid+1,r]\) 一定大于 \([l,mid]\),而不是真正的有序
P5445 [APIO2019] 路灯
思路
依旧是正难则反,考虑每一时刻那些区间是可行的,显然对于一个极长的全 \(1\) 序列 \(l,r\),所有 \(l\le x \le y \le r\) 都可行,每一时刻就是这些区间的并集
这样显然复杂度不过关,考虑优化
我们发现,单次修改最多只会造成 \(3\) 个区间的变化,因此我们树套树时维护三个值 \(tim\):未被删除的区间最近一次加入时刻之和,\(sum\):已经被删除的区间存在时间总和,\(cnt\):未被删除的区间个数,那么 \(t\) 时刻的答案就等于 \(sum+t*cnt-tim\),至于存在区间的维护用set即可
启发
区间问题的一种想法:二维数点
对于时间时刻的拆分
P4690 [Ynoi Easy Round 2016] 镜中的昆虫
好题,lxl牛逼
思路
从简单到困难考虑
无修改,区间查
记录对于一个数 \(a_i\),记录 \(b_i=max_{j=1}^{i-1} j\ , a_j=a_i\),答案就是区间内 \(b_i < l\) 的个数,扫描线+树状数组即可
单点修,区间查
直接修改 \(b_i\) 即可,用链表维护
区间修,区间查
看似复杂度是 \(O(nm)\) 的,但实际上对于一个连续颜色段来说,只有左端点的 \(b_i\) 由整体的颜色决定,其余的都是 \(b_i=i-1\),因此我们每次只需要修改颜色段的端点即可,由于一个颜色段最多删除一次,每次操作最多只会增加一个段,修改次数是均摊 \(O(n+m)\) 的
启发/反思
颜色段均摊有两层含义,都是建立在存在区间赋值(推平)操作的基础上。
- 随机数据:任意时刻的颜色段个数期望 \(O(\log_2n)\);
- 非随机数据(本题):每次推平时访问的颜色段个数为均摊 \(O(n)\)。
P3165 [CQOI2014] 排序机械臂
思路
区间查第 \(k\) 小显然是不可做的,但是由于本题的性质,第 \(i\) 小相当于 \([i,n]\) 中的最小值
注意有题目对于大小相同的数的要求,需要先赋一个排名
启发/反思
没有想到最小值的转换
P3987 我永远喜欢珂朵莉~
思路
整除的均摊复杂度是 \(O(n \log n)\) 的,难点在于如何找出区间内被 \(x\) 整除的数
发现约数的个数并不多,因此可以对每个约数开一颗平衡树,每棵树内维护被 \(x\) 整除的数,区间操作直接遍历整除即可,同时将不能被整除的删去
但是还有一种更优的做法:使用路径压缩的链表/并差集实现平衡树的功能
启发
并查集/路径压缩思想的应用:可删链表的维护


浙公网安备 33010602011771号