数据结构总结
P4198 楼房重建
非常好题目,首先你显然能够得到一个楼房看得见的条件:当斜率严格大于之前的所有斜率时,这栋楼房可以被看见。
接着我们考虑线段树 \(sum_i\) 维护 \([l,r]\) 从 \(l\) 出发可以看到的楼房数。我们发现重点在于 push_up 函数的实现,设左区间为 \(ls\),右区间为 \(rs\)。 那么 \(sum_s\) 必然要加上 \(sum_{ls}\)。接着考虑右区间,如果 \(mx_{ls} < a_{mid+1}\) 那就直接加上右区间答案;如果 \(mx_{rs} \leq mx_{ls}\) 直接返回 \(0\);如果右区间的左区间的最大值大于左区间的最大值,那就递归下去并加上右区间的右区间的答案;不然就递归右区间。
P7470 岛屿探险
首先看到下面给的部分分,就提示你分类讨论:一种是 \(b>d\),另外一种是 \(b \leq d\)。
1. \(b > d\)
这种你会发现这个只跟 \(d\) 有关,你对所有的 \(a\) 建一颗 trie 树然后遍历就行了。
具体地,就是对于当前 \(d\) 在二进制下第 \(i\) 位为 \(p\),\(c\) 在二进制下第 \(i\) 位为 \(q\)。若 \(p\) 为 \(1\),那我在这一位上如果选择 \(a\) 在这一位上为 \(q\),后面就都不需要继续考虑,直接加上有多少个这样的 \(a\)。接着继续向下遍历即可。
2. \(b \leq d\)
这种乍眼一看好像不好解决,但你发现这个跟第一种是同构的,只需将所有需要考虑的 \(c\) 建一颗 trie 就行了,做法与第一种类似。
现在我们知道了如何解决这两种操作,但如何在有限时间内求出所有答案?我们考虑线段树分治,将每个查询 \([l,r]\) 拆开放到线段树上,这样就可以对所有查询同时处理了。
但是,\(b\) 和 \(d\) 都有多个,所以我们考虑双指针。先将岛屿和询问分别按照 \(b\) 和 \(d\) 的大小升序排列,接着遍历 \(d\),每次再把不符合 \(b>d\) 的 \(b\) 剔除 \(a\) 的 trie,再讲这个 \(a\) 去遍历一遍 \(c\) 的 trie 即可。
复杂度: \(O(nlognlogw)\)
P6018 Fusion tree
思路很简单,只需要把父亲和儿子分开考虑就行,维护儿子需要全局加 \(1\) 的操作,trie 树按位从小到大建,每次交换 \(1\) 和 \(0\) 两棵子树,具体代码:
inline void modify(int u){
swap(trie[u][0],trie[u][1]);
if(trie[u][0]) modify(trie[u][0]);
push_up(u);
}
P4768 归程
题目可以转化为当前能走到的点中与 \(1\) 最近的距离,所以先从 \(1\) 出发跑最短路。
而当且能走的边为 \(> p\) 的边,而我们只需要知道这个点是否能从我们的出发点到达,所以用 kruskal 重构树,先求出最大生成树,然后将每个边当成合并两个连通块的点,以最后加入的点为根,点权为原来边权,那建出来的树就具备以下性质:子树内所有点两两点的所有路径中的边权最小值大于等于当前这个节点的权值,代码具体如下:
int now=n;
for(int i=1;i<=m;i++){
int x=getfa(e[i].u),y=getfa(e[i].v);
if(x==y) continue;
now++;
a[now]=e[i].w;
ed[now].push_back(x),ed[now].push_back(y);
fa[x]=fa[y]=now;
}
最后倍增查找即可。
P3402 可持久化并查集
重点在将 \(fa\) 数组可持久化,注意要按秩合并,要么按 \(siz\) 要么按 \(height\)。
和可持久化线段树相同,每个版本保留未更改的部分压缩空间。
P8253 如何正确地排序
首先对于 \(m=3\) 的情况,可以求每一个值得贡献,而对于 \(a_{x,i}\) 有贡献,当且仅当:
设另外两行为 \(y\),\(z\)。
则可以得到:
\(\left\{ \begin{aligned} &a_{x,i}+a_{x,j} < a_{y,i}+a_{y,j}\\ &a_{x,i}+a_{x,j} < a_{z,i}+a_{z,j} \end{aligned} \right. \)
或
\(\left\{ \begin{aligned} &a_{x,i}+a_{x,j} > a_{y,i}+a_{y,j}\\ &a_{x,i}+a_{x,j} > a_{z,i}+a_{z,j} \end{aligned} \right. \)
这就是一个二维偏序,就可以做了。
那么对于 \(m=4\),就是三维偏序,可以 cdq 分治做;也可以找哪一些点没有贡献,就是二维偏序了,总贡献减去这些点即可。
P5163 WD与地图
首先考虑将删边变为加边。
接着发现可以二分一条边能把两个强连通块合并的时间,于是整体二分做就行了。
P5609 对数据结构的爱
挺有思维难度的一道题,你发现题意就是未经过“取模”的和减去有“取模”位置的个数再乘以 \(p\)。
首先发现可以维护在 \([l,r]\) 这一个区间中最小需要前缀值为多少,能使有“取模”的位置有 \(x\) 个,记这个前缀值为 \(w_x\),而这个序列 \(w\) 显然是不减的。然后你就可以放到线段树上维护了,可是问题在于如何将两个不减的序列合并。
显然,\(w_x - w_{x-1} \ge p\)。
然后我们对于左区间有 \(x\) 个,右区间有 \(y\) 个位置,在合法的情况下,得到的前缀值 \(f(x,y)=max(c_y - leftsum + px, c_x)\),至于合法,就是最大的左区间有 \(x\) 个的值减去 \(px\) 之后依旧大于等于 \(c_y\)。
然后就有一个性质: \(f(x,y) \ge f(x+1,y-1)\)
证明:
\(f(x,y)=max(c_y - leftsum + px, c_x)\)
\(f(x+1,y-1)=max(c_{y-1} - leftsum + p(x+1), c_{x+1})\)
\(c_{x+1}-c_{x} \ge p\)
\(c_y - c_{y-1} -p \ge 0\)
证毕。
然后就可以在 \(O(n)\) 的时间内合并了。
P5280 线段树
首先你肯定会想到维护当前这个点值为 \(1\) 的个数,那么对于一个 \([l,r]\) 的修改,有以下几种点,设这个点 \(i\) 在线段树上表示的区间是 \([L,R]\)。
-
\([L,R] \cap [l,r] \neq \oslash, [L,R] \nsubseteq [l,r]\)
-
\([L,R] \subseteq [l,r], fa_i \nsubseteq [l,r]\)
-
\([L,R] \cap [l,r] = \oslash, fa_i \cap [l,r] \neq \oslash\)
-
\([L,R] \subseteq [l,r], fa_i \subseteq [l,r]\)
-
\([L,R] \cap [l,r] = \oslash, fa_i \cap [l,r] \neq \oslash\)
然后你会发现若一个区间能被赋为 \(1\),当且仅当它到根这条链上有值为 \(1\) 的点。
所以维护两个数组 \(f,g\),\(f_u\) 表示这个点 \(u\) 值为 \(1\) 的个数, \(g_u\) 表示点 \(u\) 到根没有值为 \(1\) 的个数。
设 \(t\) 为第 \(t\) 次修改操作。
那么对于第一种,就有转移:
\(f_u = f_u\),\(g_u = g_u + 2^{t-1}\)
第二种转移:
\(f_u =f_u +2^{t-1}\),\(g_u =g_u\)
第三种转移:
\(f_u =f_u + (2^{t-1} -g_u)\),\(g_u =2g_u\)
第四种转移:
\(f_u = 2f_u\),\(g_u = g_u\)
第五种转移:
\(f_u = 2f_u\),\(g_u = 2g_u\)
然后线段树上维护就行了。
根号数据结构
莫队+离线维护更改值 \(\to\) 二次离线莫队。
非常好理解的东西,比如说查询区间逆序对个数,在莫队的时候加一个点或删一个点显然是无法做到 \(O(1)\) 的,这时候我们就把问题离线下来,比如右端点 \(r\) 向外扩展,那么设贡献为 \(F(r+1,l,r)\) 表示 \(r+1\) 这一个点将和当前的 \([l,r]\) 做贡献,那么可以把这个贡献写成 \(F(r+1,1,r) - F(r+1,1,l-1)\) 的形式,由于莫队扩展以及收缩的的操作数是 \(O(n \sqrt n)\) 的,所以可以离线下来把值的贡献计算出来。
例题也不多,就列三个出来,感觉都挺板的。