冯梓轩数据结构总结

数据结构学习总结

前言

原先我以为,只要是纯数据结构学好了,那数据结构题就能很简单就做出来了。

后来才发现,这句话在十年前也许是对的,但现在完全错了,因为 现在是算法和思维主导的时代,数据结构在大部分情况下只是用来辅助快速求解的工具,已经不会再出纯数据结构的偏、难、怪题了。一些题既然你想不出来,不会做,那还提什么数据结构优化呢? 所以说,思维也是非常重要的,用老师的话说就是:“别把技能点点偏了”。

并查集

并查集可以方便的处理一些与合并相关的问题,如区间信息合并、树上合并等,而且代码小巧,非常好用,如果只用路径压缩,时间复杂度为 \(O(n \log n)\),加上按秩合并,时间复杂度可以优化为 \(O(n \alpha(n))\),但是实际上没有什么太大的区别,而且在对合并有顺序要求时不能按秩合并。

并查集一般与其他知识点结合,虽然大部分情况下比较难想一些,但是代码好写,且时间复杂度一般会相对优秀些。

例题

恢复

正着做比较麻烦,考虑倒着做,从所有点都已恢复的状态逆推回所有点都未恢复的状态。如果没有树形的限制,那就是一道很简单的贪心,这里不再赘述。现在考虑有先后顺序的限制,有一个结论是:如果恢复了一个点,那么下个恢复的点要尽量是它的儿子,这样做一定不劣。反过来操作就是:如果把一个点从恢复变为未恢复后,一定先计算它和它的父亲之间产生的贡献。计算之后,为了方便计算,可以把它所在的并查集和它父亲所在的并查集进行合并,新的点的权值就是两个小并查集的权值和。由于要动态维护全局最大值,所以考虑使用大根堆进行维护,每次合并后,都将其再次放入堆中。

单调队列优化 DP

对于一种 DP:\(f_i = \max\limits_{j=l}^{r} f_j + w(j,i)\)\(f_i = \min\limits_{j=l}^{r} f_j + w(j,i)\),其中 \(r-l+1\) 为一定值,可以考虑维护一个滑动窗口,在线性时间内求得所有 DP 值。

例题

【SCOI 2010 DAY1】股票交易

肯定要把天数放入状态中。由于计算钱数需要手中持有的股数,所以考虑将其加入状态中。

\(f_{i,j}\) 表示经过了前 \(i\) 天后,手上持有 \(j\) 股的最大利润。

方程显然为:

\[f_{i,j} = \max \left \{ \begin{aligned} &f_{i-1,j} \thinspace \texttt{(第} \space i \space \texttt{天什么也不干)} \\ &f_{i-w-1,k}-(j-k) \times ap_{i-w-1} \thinspace (j-k \leq as_{i-w-1}) \thinspace \texttt{(在第} \space i-w+1 \space \texttt{天买入} \space j-k \space \texttt{股)} \\ &f_{i-w-1,k}+(k-j) \times bp_{i-w-1} \thinspace (k-j \leq bs_{i-w-1}) \thinspace \texttt{(在第} \space i-w+1 \space \texttt{天卖出} \space k-j \space \texttt{股)} \end{aligned} \right. \]

第一个方程显然可以 \(O(1)\) 转移。对于第 \(2\) 个方程,如果暴力转移,求出一个 $f_{i,j} $需要 \(O(n)\),无法接受。观察到方程中的转移是一段定长区间,且 \(j\)\(k\) 互不干涉,可以考虑用单调队列优化,在 \(O(n)\) 的时间内求出所有 \(f_i\) 的值。

线段树

对于一些比较复杂的区间操作问题,可以使用线段树来优化时间复杂度,还可以用来优化 DP 等算法。

例题

【NOI2016】区间

如果对右端点进行排序的话,无法很好的维护区间长度信息,所以考虑换种方式,按照区间长度进行升序排序。

观察到一个单调性:被覆盖次数最多的点的被覆盖次数随着线段的增多单调不降,可以考虑用双指针维护,钦定一个左端点,一直向右加入线段,直到最大覆盖次数等于 \(m\),这样答案就可以 \(O(1)\) 算出。对于加入线段,发现是单点加,区间取 \(\max\),可以使用线段树快速维护。

[SDOI2009] 虔诚的墓主人

对于一个点 \(i\),它对答案的贡献为:\(\tbinom{up_i}{k} \times \tbinom{down_i}{k} \times \tbinom{left_i}{k} \times \tbinom{right_i}{k}\),其中 \(up_i,down_i,left_i,right_i\) 分别表示点 \(i\) 上面、下面、左边、右边的点的数量。对于这种问题,我们通常可以用线段树来维护。设当前扫到的横坐标为 \(x\),对于两个横坐标为 \(x\) 的点 \(p,q\),它们中间夹的点一定是空白点,而这些点的 \(up\)\(down\) 值都可以边做边推,所以只需要求出 \(\sum\limits_{i=y_p+1}^{y_q-1} \tbinom{left_{x,i}}{k} \times \tbinom{right_{x,i}}{k}\) 即可,而这与线段树扫描线类似,考虑用线段树维护,每次加入一个点都重新计算对应 \(y\) 坐标的值,然后区间求和。

【ZJOI2010 Day1】基站选址

显然是 DP。设 \(f_{i,j}\) 表示建了 \(j\) 个基站,其中最后一个基站建在了点 \(i\),且前 \(i\) 个位置被全部覆盖的最小费用,方程为:

\[f_{i,j} = \min\limits_{k=1}^{i-1} f_{k,j-1} + pay(k,i) + c_i \]

这里的 \(w(k,i)\) 表示区间 \([k,i]\) 中没有被覆盖的点所需的赔偿费用。

可以发现,第 \(2\) 维状态没什么用,只需先枚举基站,再枚举位置,就可消掉,方程变为:

\[f_{i} = \min\limits_{k=1}^{i-1} pre_{k} + pay(k,i) + c_i \]

假设点 \(i\) 只要在点 \([l_i,r_i]\) 任意放置了一个基站就能被覆盖。如果在区间 \([l_i,r_i]\) 中放置了一个基站,就不会要求补偿,但是如果被转移的点 \(j > r_i\),那么在使用 \([1,l_i)\) 转移时就不能覆盖到 \(i\),所以要将 \(pre_j\thinspace(j \in [1,l_i))\) 都加上 \(w_i\)。但是这样做的时间复杂度是 \(O(n^2m)\) 的,发现我们需要区间询问一个最小值,然后修改时单点修改,这可以使用线段树进行维护,时间复杂度优化为 \(O(nm \log n)\)。最后对于要把所有点都覆盖,可以设置一个虚点,其 \(D_i\) 为无限大,\(C_i\)\(0\)\(S_i\)\(0\)\(W_i\) 为无限大。

小结

对于很多题目,暴力做或者用其他方法维护的话可能难以维护或时间复杂度爆表,这时候可以使用数据结构优化维护,特别像 DP 一样,通常可以使用数据结构加速转移。但是要适当使用数据结构,谨记一句话:“不要迷信数据结构”

posted @ 2024-02-04 14:02  gevenfeng  阅读(25)  评论(0)    收藏  举报