常用数据结构

常用数据结构

过于基础的就不提了,主要记一下算法题里面会用到的, 从应用场景记;

1. 单调栈 / 单调队列

1. 单调栈

单调栈: 给定一个序列,求序列中左边/右边离它最近的且比它小/大的数

以找出每个数左边离他最近的比它小的数为例

2. 单调队列

单调队列: 滑动窗口的最值

记住单调队列存的是下标 方便判断是否已经划出窗口

2. 并查集

并查集可以说代码短又好用第一名,我们记的都是带路径压缩的并查集

1. 朴素并查集

2. 维护size的并查集

这里注意 只有每个集合root的size才有意义哦!

3. 维护到祖宗节点距离的并查集

4. 拓展域并查集

拓展域并查集的思想: 枚举 在带拓展域的并查集中,同一个集合内,只要有一个条件成立,则集合内所有元素的条件都成立

这里还是举个题来解释一下比较好(带权并查集也会举这个题来解释)

奇偶游戏 avatar

除了并查集之外 这题还有点别的处理,sum[N]表示有从S[0...i] 1的个数(前缀和)

这样,题中区间1的个数就转换为:

  • 如果S[L ~ R] 有奇数个1 -> sum[L - 1] 和 sum[R] 的奇偶性相反

  • 如果S[L ~ R] 有偶数个1 -> sum[L - 1] 和 sum[R] 的奇偶性相同

这里带权并查集就把每个元素的可能性都枚举出来:

x 表示 如果x是偶数

x + n 表示 如果x是 奇数

A. x, y 是同类

    1. x是奇数, y也是奇数 => merge(x + n, y + n);

    1. x是偶数, y也是偶数 => merge(x, y);

B. x, y 是不同类

    1. x是奇数, y是偶数 => merge(x + n, y);

    1. x是偶数, y是奇数 => merge(x, y + n);

查询中 如果每种都不成立 则证明一定是矛盾的

5. 带权并查集

带权并查集的思想:偏差量

带权并查集的代码和维护到祖宗节点的并查集很像, 是通过维护与a, b 与根节点的关系(距离)来推导出a, b间的相对关系

比如本题 如果root 和 a 奇偶性相同 root 与 b 奇偶性相反 => a与b奇偶性相反

可以用异或维护, 更常见的是用0 ~ m - 1 表示状态 用mod操作来维护状态(环形关系) 指路食物链这个题

带权并查集和普通并查集在find函数和merge操作上有区别

merge操作要补上从papb的路径长度 avatar

3. 双链表

4. 树状数组

1. 树状数组原理

树状数组(FenwickTree) 的场景: 快速求前缀和 + 单点修改(修改数组其中某一个数) 这两个操作都是o(logn)

avatar
avatar

很重要一个性质:parent(i) = i + lowbit(i)

1. 单点更新

记住 记住 这里更新的是差值:新值 - 原值

2. 求前缀和

2. 树状数组 + 差分

树状数组用来解决单点更新和区间求和问题;那么对于对等问题 区间更新 和单点求值 要怎么解决呢?

差分+ 树状数组, 差分的思想实际就是前缀和的逆运算:

insert(l, r, val) 可以拆解为b[l] += val, b[r + 1] -= val;

b的前缀和数组实际上就是a;

这里我们把这个思想和树状数组加到一起

3. 树状数组 + 置换k次最小字典序

5. 线段树

线段树的原理这里就不硬扯了,虽然线段树能解决的题很多,但大概的数据结构框架都长一样(写错就是SegFault...) 有以下五个操作:

  • pushup

  • pushdown(区间修改要用到)

  • build

  • modify

  • query

1. 单点修改

单点修改就用无需懒标记的线段树就好(懒标记能不写就别写了 正常的还写不对呢)

线段树维护的信息除了询问的答案之外,还要考虑能否从左右两个区间推断出答案, 如果不行要额外维护信息

举几个例题看一看吧~

1. 最大数

给定一个正整数数列 a1,a2,…,an,每一个数都在 0∼p−1之间。

可以对这列数进行两种操作:

添加操作:向序列后添加一个数,序列长度变成 n+1 询问操作:询问这个序列中最后 L 个数中最大的数是多少。 程序运行的最开始,整数序列为空。

写一个程序,读入操作的序列,并输出询问操作的答案。

输入格式

第一行有两个正整数 m,p,意义如题目描述;

接下来 m行,每一行表示一个操作。

如果该行的内容是 Q L,则表示这个操作是询问序列中最后 _L_个数的最大数是多少;

如果是 A t,则表示向序列后面加一个数,加入的数是 (t+a) mod p。其中,t是输入的参数,a 是在这个添加操作之前最后一个询问操作的答案(如果之前没有询问操作,则 a=0)。

2. 最大连续字段和

给定长度为N的数列A,以及M条指令,每条指令可能是以下两种之一:

1、“1 x y”,查询区间 [x,y] 中的最大连续子段和,即 maxx≤l≤r≤y {∑ri=lA[i]}。

2、“2 x y”,把 A[x] 改成 y。

对于每个查询指令,输出一个整数表示答案。

这个题显然只维护一个最大连续子段和是没有办法从左右子区间推出来当前区间的最大连续子段和的;

(可能有连续字段和横跨两个区间的情况) 所以这里要额外维护前缀和 后缀和 区间和 来实现最大连续字段和的传递(pushup)

具体看代码吧

3. 区间最大公约数

给定一个长度为N的数列A,以及M条指令,每条指令可能是以下两种之一:

1、“C l r d”,表示把 A[l],A[l+1],…,A[r] 都加上 d。

2、“Q l r”,表示询问 A[l],A[l+1],…,A[r] 的最大公约数(GCD)。

对于每个询问,输出一个整数表示答案。

区间最大公约数怎么考虑呢 有这样一个公式

(a, b, c, d ...) = (a, b - a, c - b, d - c, ....

这样区间求和我们就把他和差分联系起来,实际上线段树维护的是差分数组的gcd值,但是除了后面的差分值之外 还有一个a 实际上是差分数组的前缀和 所以还要额外维护一个sum值

2. 区间修改(懒标记)

3. 扫描线法

6. treap

7. 可持久化数据结构

8. Trie 树 (可删除)

正常的Trie树

数组模拟链表 搞struct效率会低一些

支持delete操作的Trie树其实用的挺少的 用到了就来这查吧hh

9. KMP

默写就完事了

10. 字符串哈希

场景:快速判断子串是否相等

字符串哈希很好用 有一大半的字符串题都可以用字符串哈希来解决(剩下估计就是KMP 还有模拟了ha)

这里 P 的经验值有131 和 13331 两种,存储哈希值用的是unsigned long long 这样mod(2 ^ 64) 可以省略(溢出直接相当于取模了)

11. 前缀和与差分

1. 前缀和

场景:快速求区间和

  1. 一维前缀和 S[i] = a[1] + a[2] + ... a[i] a[l] + ... + a[r] = S[r] - S[l - 1]

  2. 二维前缀和 S[i, j] = 第i行j列格子左上部分所有元素的和 以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为: S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

2. 差分

  1. 一维差分

场景:多次使区间[l, r]中的每个元素同时增加or减少k

差分实际上是前缀和的逆运算 构建数组b使得 b的前缀和数组等于a

给区间[l, r]中的每个数加上cB[l] += c, B[r + 1] -= c

  1. 二维差分

给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上cS[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c

12. 离散化

离散化有两种:保序离散化和非保序离散化

非保序离散化就unordered_map 这里我们用到的是保序离散化(维持离散化后的大小关系)

13. 高精度

一般处理一些字符串加减乘除的问题 还有高精度&低精度的一些算法

1. 高精度加法

记住那个进位t就好

2. 高精度减法

和高精度加法是类似的 只不过有一个 借位的问题

  1. 借位怎么处理

  2. 取余要取到整数

  3. 前导0记得弹出来

14. RMQ

多次查询区间最大值 -> 二进制倍增

Last updated