CST数据结构(2020秋)PA3
3.1 Not Found
算法
要找二进制字符串 A 中最短的未出现过的子串 B,我们先考虑一个比较长的子串,其长度为 24。 注意到 A 的长度最长为 16777216 = 2^24。因为还要掐头去尾,所以 A 中长度为 24 的子串的总数必定小于 2^24 个,而长度为 24 的字符串总共有 2^24 种,所以 A 中必定有某个长度为 24 的字符串是不存在的。
我们用 bitmap 边读入,边记录下所有出现过的长为 24 的子串。这个 bitmap 只存长度为 24 的子串,我们叫它 bitmap24。读入完成后,注意到任何一个在 A 中出现的长为 23 的子串必定是某一 24 子串掐头或去尾得到的,于是我们遍历所有在 24 子串,对他们掐头去尾,将得到的两个结果存入 bitmap23 中,如此做直到 bitmap1 存完。
最后我们从长度 24 开始遍历,找到第一个长度 n 使得所有长度为 n 的子串都在 A 中出现了,那么所要找的“最短未出现子串” B 必然有长度 n+1,我们只需要再遍历一遍 bitmap(n+1) 找到第一个不存在的字符串即可
细节
- 读入字符串的时候当总长度达到 24 以后,我们就要读一个新的弃一个旧的,因为根据题目分析 B 最长也就是 24
- 一个 int 是 4 byte = 32 bit = 2^5 bit,所以 bitmap24 需要 $2^{24}/2^5 = 2^{19}$ 个 int,bitmap1 … bitmap 5 各自仅需 1个 int
- 因为我们是将二进制字符串用 int 方式存在 bitmap 中,如果这个字符串有 leading 0s, 它们在输出时会被忽略掉,所以我们需要根据 bitmap-n 这个长度 n 来补全 leading 0s
代码
1 |
|
复杂度分析
读入长度为 n 的字符串,耗时 O(n)
如果 n >24 则从 bitmap24 开始遍历,如果 n<=24 则从 bitmap(n) 开始遍历,耗时 $O(2^{min(24,n)})$
当 n 达到 2^24 级别时,整体复杂度还是 O(n)
Reference
3.3 Kth
算法
题目要求找出 a,b,c 三个数组对应的三元数对中和为第 k 大的那个三元数对,观察到如果 a,b,c 是有序数对,那么必有 a[i]+b[j]+c[k] < a[i+1]+b[j]+c[k]
, a[i]+b[j]+c[k] < a[i]+b[j+1]+c[k]
, a[i]+b[j]+c[k] < a[i]+b[j]+c[k+1]
.
于是,我们维护一个优先队列,每次出队 (i,j,k)
就入队 (i+1,j,k) (i,j+1,k) (i,j,k+1)
。如此做 k 次,出队的就是我们要找的三元对。我们现在将“找第 k 大”转变成了一个三维图的遍历问题。
实现中要注意的是不能让同一个点多次入队,我们可以开一个 vis 数组,但是每个数组最多有 500000 个元素,三维 vis 数组空间绝对不够。于是我们想一种遍历顺序,使得每个点只被遍历一次。首先考虑最简单的一维,单个的 x 轴,就是不停地遍历下一个而已 i, i+1, i+2, ...
;扩展到二维其实就是多个一维情况,我们通过 (0,j), (1,j), ... (i-1,j)
到达 (i,j)
那我们如何到达 (0,j)
呢?通过 (0,0)
的一维扩张,也就是说,当 x 轴为 0 时,我们既向 x 方向扩张,也向 y 方向扩张,而当 x 轴不为 0 时,我们只向 x 方向扩张。
对于三维情况,想象 x,y,z 正方向为右,前,下。则在任意时刻,我们都向 x 扩张;仅当 x=0 时,我们向 y 方向扩张;仅当 x=0 且 y=0 时,我们向 z 方向扩张。并且由于我们根据优先级选取每一次的扩张边界,我们一定也是优先级最高的先被找到。
细节
- Heap 的实现:
sink
时首先判断孩子存不存在(孩子坐标与元素总数比较)如果左孩子存在且“右孩子不存在,或左孩子优先级比右孩子高”,则与左孩子互换;如果右孩子存在且右孩子优先级更高,则与右孩子互换 - 三维的遍历顺序:尝试向 y 方向扩张时,如果
x!=0
,跳过此次扩张;尝试向 z 方向扩张时,如果x!=0 || y!=0
,跳过此次扩张 - 数组的排序:在本题提供接口中,我们无法直接访问数组 a,b,c 中的元素,所以我们自己开另外三个数组 s,u,t 其中 s[i] 表示 a 中第 i 大的元素所对应在 a 中的位置。即 s,u,t 存 1…n, 代表 a,b,c 中的下标。为取得 s,我们使用
sort(s,n)
但比较器用的却是 a 的比较器
代码
1 |
|
复杂度分析
共有三个数组,一个数组中有 n 个元素,找大小为 k 对的三元数对。首先对三个数组进行排序 O(nlogn),每有一个数对出优先队列,就有最多三个入优先队列,共操作 k 次,每次操作 O(logk),总共 O(klogk)。总时间 O(nlogn + klogk)
3.4 Component
算法
堆的合并 左偏树
题目的询问永远是某一联通分量中第 k 大的点的权值,k 是一个常数。第 k 大又可以看做前 k 个最大元素中最小的元素,即如果我们维护一个小根堆,使它恒有 k 个元素(n<k 时输出 -1,n>k 时弹出 n-k 次最小的元素)那么这 k 个元素必然是连通块中前 k 大的元素,堆顶元素就是我们的询问。
当加入的新边 (u,v) 联通两个不曾联通的连通块时,对应的两个堆必须合并。支持快速合并操作的优先队列,我们选择左式堆。(u,v) 将块联通,实际上是将其所在的堆合并起来,我们必须能够高效找到 (u,v) 所属哪个堆,即其所属堆的根是谁,使用并查集存储这个信息。
细节
每个节点都有编号,我们不用传统的 class 建优先级队列,而直接用数组存每个点对应的信息,速度更快,访问更方便
代码
1 |
|
复杂度分析
初始化后,每个点最多被入堆一次(所在连通块与他人联通),出堆一次(因为不属于前 k 大而被弹出堆)每次出入堆操作是两个左式堆的 merge,复杂度 O(logn)。共 n 个点,所以总体复杂度是 O(n logn)