国庆郑州集训day3:数据结构

来源:互联网 发布:p2p理财系统源码 编辑:程序博客网 时间:2024/04/28 04:56

数据结构

哈希字符串

字符串Hash:一种从字符串到整数的映射。

BKDR-Hash

》竞赛中常用的 hash 策略
》把字符串视为一个 base 进制的大整数,对某个质数 P 取模得到 hash 值
》sum_i = (sum_{i-1} *base + str_i) mod P
》base 可以取 31、131、13131 等,需要满足 base > |字符集|
》P 取 long long 范围内一个质数,注意溢出问题
》使用 unsigned long long 自然溢出可以视为对 2^64 取模
》但是可能被卡(对任意base)
》害怕 Hash 被卡的同学,也可以选择双 hash(常数翻倍)。

常用技巧

给定字符串 S,预处理出它的前缀 Hash 函数;同时计算好 mod P 意义下 base 的幂次表
sum[i] = (sum[i - 1] * base + str[i]) % P ////sum[i]为前i位的hash值
pw[i] = (pw[i - 1] * base) % P
基础应用:
提取一段子串的 hash 值
合并两个串的 hash 值
O(\log n) 计算两个子串的 lcp 和字典序大小

P:企鹅QQ

给定 N 个长度均为 L 的串
问有多少对字符串满足:恰好有一位对应不同
N <= 30000, L <= 200

ans:枚举删掉每一个位置,用 Hash 来进行答案统计

Trie树

又称字母树,可以用来维护字符串集合
优化思想是,利用字符串的公共前缀来减少查询时间,最大限度地减少无意义的比较
结构:有根树,每条边上存有一个字符
从根到每个叶子的路径上经过的字符写下来,对应了一个字符串

支持插入、查找、删除。

经典例题

(1)
给定 2 N 个字符串,你需要将它们配对起来
两个字符串 x、y 配对的得分是它们的 lcp 长度
最大化得分
N*<= 10^5,字符串总长 <= 2 * 10^6
建出 trie 树,两个串的 LCP 即为它们的 LCA 的深度
使用贪心算法,按照树的 DFS 序列配对

(2)
给定一棵有根树,每条边有权值 w_i
求树上的一条简单路径,使得路径经过的边权异或和最大
N <= 2 * 10^5, w_i <= 10^9
记录 dis[a] 表示 a 到根的链的异或和
考虑 x、y 之间的链的异或和,设 LCA 为 z
= (dis[x] ^ dis[z]) ^ (dis[y] ^ dis[z]) = dis[x] ^ dis[y]
不难发现与 z 无关!
于是问题转化为,给定 N 个数,选出两个使得异或和最大
考虑枚举两个数之一 x,我们想在其它数中找到一个与 x 的异或和最大
从高到低考虑每一位,尽可能让更高位为 1
不难发现可以使用 Trie 树!
复杂度 O(N * 32)

并查集

原理

对每个集合,建立一个有根树的结构
令树的根为整个集合的“代表”
想知道两个元素是否在同一集合,只需比较它们的代表
合并时,将一棵树接到另一棵下边即可

优化策略

路径压缩
按秩合并(不考虑路径压缩的情况下,秩为该树的最大深度(最深叶节点的深度))
可以证明,使用这两种优化的并查集复杂度为 O(α(n))
绝大多数情况这个值不大于 5,可以认为是线性的

应用

最小生成树的 Kruskal 算法
Tarjan’s off-line LCA Algorithm

带权

在一些应用中,可以在每个点上额外维护一些信息,表示“它与父亲”之间的关系
进而尝试推算集合中任意两个元素之间的关系

经典例题

帮派:
某市有两个帮派,有 N 个人,每个人属于两个帮派之一。
给定 M 个事件:
1 x y,表示告诉你 x 和 y 属于同一帮派
2 x y,表示告诉你 x 和 y 不属于同一帮派
3 x y,表示请你推理 x 和 y 之间的关系
N <= 5 * 10^5, M <= 10^6
给每个人额外维护一个标记 rel[x] 表示 x 和 x 的父亲的关系
由 rel[x] 和 rel[fa[x]] 可以推算 x 和 fa[fa[x]] 的关系。。。以此类推可以推算 x 和 Root[x] 的关系
于是任意两个人只要在同一连通块,就能推算他们的关系
问题:这个并查集如何使用路径压缩优化呢?

只按秩合并

经典例题

给定 N 个点,支持 M 个操作:
1 x y,在 x 和 y 之间连边
2 x y,询问 x 和 y 是否连通,如果是,那它们最早在哪一次操作之后连通的
N <= 2 * 10^5, M <= 5 * 10^5
@货车运输
离线的时候可以建树倍增 blabla。。。
强制在线呢?
只按秩合并,link(x, y, tim) 时,我们在 Root[x] 和 Root[y] 之间连一条边权为 tim 的边
询问 (x, y) 时,找到 x 和 y 之间边权最大的边即可
这种算法‘的复杂度是容易证明 O(\log N) 的
正确性?

优先队列

支持这样几种操作的数据结构:
插入一个优先级为 key 的元素
询问优先级最高的元素
删除优先级最高的 / 任意一个元素
升高一个元素的优先级值
优先队列一般使用堆来实现
最经典的堆即为大名鼎鼎的二叉堆

二叉堆

二叉堆是一个完全二叉树结构,并且它具有堆性质:
每个点的优先级高于它的两个孩子(如果有)
可以用一个数字来存储二叉堆,避免指针:
1 是根结点
对于 x,它的左右孩子分别是 2x 和 2x+1
容易验证 N 个点的二叉堆,它用到的数组即为 1 ~ N
给定一个大小为 N 的数组,我们可以 O(N) 的建堆(How?

操作

随着操作的进行,二叉堆的“堆性质”可能会遭到破坏,为此我们定义两种调整操作,来维护二叉堆的堆性质保持不变
向上调整:
当一个点的优先级升高时,我们需要向上调整
比较它和它的父亲的优先级,它的优先级高就与父亲交换位置并递归进行
向下调整:
当一个点的优先级降低时,我们需要向下调整
比较它和它左右儿子中优先级较高的那个,它的优先级低就与儿子交换并递归下去
容易验证两种操作的复杂度均为 O(\log N)

线段树入门

线段树是一种二叉搜索树,一般可以用来维护序列的子区间

结构

对一个长度为 n 的序列建线段树,根结点即表示 [1, n]
对于一个表示 [l, r] 的节点:
若 l = r,则它是叶子
否则,令 m = (l + r) / 2,它有左右两个孩子,分别记为:[l, m] 和 [m + 1, r]
不难验证,这样一个线段树中有 2N - 1 个节点,并且树的深度是 O(\log N) 级别

原理

线段树的优化思想:
根据问题的要求,用每个节点维护它对应的子区间中、可以高效合并的相关信息
在动态的序列问题中,对于修改操作没有动过的部分。我们可以考虑把这些地方的求解的结果保存并复用,从而达到优化程序效率、降低复杂度的目的

例题

(1)给定一个长度为 N 的序列,支持:
修改一个位置的值
查询一个子区间的元素和
线段树每个节点维护对应子区间的和
区间覆盖:
对于一个区间 [l, r],我们可以将其分解为线段树上 O(\log N) 个节点的并;
这里的分解是指,我们选取的区间并起来恰好为 [l, r]。且选择的区间不会相互重叠
修改操作中,为了维护线段树性质,需要修改总共 O(\log N) 个节点
查询操作,将区间拆为 O(\log N) 个区间的并,从而优化查询的复杂度
总时间复杂度 O(\log N)

(2)有 N 头牛,第 i 头牛的高度为 i
现在这 N 头牛随意站成一列,第 i 头牛看到前面有 a[i] 个牛的比它高
求每个位置的牛的高度
N <= 2 * 10^5
(3)等差子序列
给定一个 1 ~ N 的排列
问:是否存在一个长度 >= 3 的等差子序列
N <= 2 * 10^5
考虑是否存在一个以 x 为中项的、长度为 3 的等差序列
这等价于:存在 k > 0,使得 x-k 和 x+k 在 x 的两侧
我们用两个二进制数,分别记录:在 x 左侧的, 1 ~ x-1 和 x+1 ~ N 的存在情况
于是,判断 k 的存在性与判断两个二进制数的相等是等价的
从左到右扫描每一个数,用线段树维护、查询每一段连续的数的存在情况(使用 BKDR-hash),就能支持快速合并了

(4)给定一个序列,支持:
区间加上一个整数 x
询问区间的和
修改操作暴力进行,复杂度为 O(N)
使用懒标记
把修改操作分解为 O(\log N) 个线段树区间的并
在这些线段树区间上打一个“整体被加了 x” 的标记
将来必要的时候再将标记下放

树状数组

一种支持单点修改和查询前缀和的数据结构
复杂度为 O(\log N),但是常数很小

原理

定义 lowbit(x),表示将 x 写成二进制后,只保留二进制下最低一个 1 对应的整数
例:lowbit(1001100) = 100, lowbit(1000) = 1000
十进制:lowbit(76)=4, lowbit(8) = 8
对一个数组 a[],我们构造数组 c[],其中
c[i] = sum (. a[i - lowbit(i) + 1 … i] )
巧妙的事情来了:
我们查询 a[] 的前缀和只需要访问 c 中 log N 个节点
修改 a[] 中任意一个元素的值,只需要同时修改 c 中的 log N 个节点
于是可以在 O(\log N) 的时间内支持单点修改、前缀和查询

树状数组 vs 线段树

复杂度均为 O(\log N)
优点:

树状数组常数小、跑得快;线段树常 数较大,跑得稍慢
树状数组相比线段树好实现
树状数组可以比较简单地推广到二维

不足:

树状数组的功能是线段树的子集

二维树状数组

对二维数组 a[][],维护 c[][] 表示
c[i][j] = sum( a [ i-lowbit(i)+1 … i] [ j-lowbit(j)+1 … j] )
可以支持二维数组的单点修改、查询前缀矩形的和
复杂度 O(\log^2 N)

区间加+区间查询

想支持对数组 a[] 的区间加整数、区间查询和
考虑 a[] 的差分数组 b[i] = a[i] - a[i - 1]
区间加对 b[] 只修改两个位置!
同时,为了支持查询 a[] 的前缀和,经过一番推导,发现只需维护 b[i] 和 i * b[i] 的前缀和即可
复杂度 O(\log N)

原创粉丝点击