序列哈希
序列哈希可以将一个序列或者字符串以 \(O(len)\) 的时间复杂度压缩成一个 long long
(或者 int
)。当序列的数量不多的时候(\(\le 10^8\)),一个 hash
值就可以唯一代表一个序列。
我们可以使用序列哈希快速判断回文串,或者搭配哈希表用来 \(O(1)\) 检测序列是否在集合中出现过。
我们提供一种较为稳定的 \(\operatorname{hash}\):
建议 \(p\) 取 \(2^{61}-1\),\(base\) 取一个略比 \(a\) 值域大的数字。
合并和差分
已知两个序列的 \(\operatorname{hash}\) 和 \(len\),我们可以 \(O(1)\) 求出它们拼接后的 \(\operatorname{hash}\) 值(当然,需要预处理 \(base^i\)):
由于 \(\operatorname{hash}\) 时序列的每一项较为独立,我们可以通过逆元对 \(\operatorname{hash}\) 值进行差分。因此,我们可以 \(O(1)\) 求得序列区间 \(\operatorname{hash}\) 值,或者树上路径 \(\operatorname{hash}\) 值。
当然,通过移项就不需要求逆元了。
例题
P5537 【XR-3】系统设计
题意
给定一棵 \(n\) 个节点的有根树和一个长度为 \(m\) 的序列 \(a\)。你需要维护两种操作:
1 l r x
:表示设定起点为节点 \(x\),接下来依次遍历 \([l,r]\)。当遍历到 \(i\) 时,从当前节点走向它的编号第 \(a_i\) 小的儿子。如果某一时刻当前节点的儿子个数小于 \(a_i\),或者已经遍历完 \([l,r]\),则在这个点停住,并输出这个点的编号,同时停止遍历;2 p v
:将 \(a_p\) 修改为 \(v\);
显然,我们可以二分答案找到最长的合法的 \([l,r]\) 的前缀,这样问题就转化为一个判定性问题。
考虑不带修的子问题。如果保证 \(x=rt\),则可以通过预处理返根链的 \(\operatorname{hash}\) 值 \(h[u]\),并且使用哈希表维护就可以实现 \(O(\log n)\) 查询。其它情况,我们可以通过差分并移项的方式,去掉 \(1\rightarrow x\) 的贡献。
考虑如何带修,需要哪些数据结构。注意到每次 check()
本质上是查询了序列中一段区间的 \(\operatorname{hash}\) 值。\(\operatorname{hash}\) 值是支持快速合并的,因此可以使用线段树维护。这满足线段树上二分的使用条件。同时线段树上二分支持动态修改,因此可以解决本题。
哈希表的模数不能和 \(base\) 相同
哈希得到的结果仍然很大,可以使用哈希表存储。哈希表需要对哈希值进行取模,注意该模数不能和 \(base\) 相同,否则哈希表会产生大量冲突,导致时间复杂度错误。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
|