这次主要介绍伪随机数生成算法,顺便介绍一个在2018-2019年的伪随机数研究成果,就是 xoshiro/xoroshiro 随机数生成算法。
历史 在较早的时候,甚至到现在,伪随机数的生成元老级别算法“线性同余伪随机数生成算法”可谓无处不在,像现在的C/C++的rand函数就是使用线性同余实现的伪随机数生成。所谓的线性同余法,就是这个迭代方程 $S_n = (aS_{n-1} + c)\mod m$,其中,$S_0$ 称为这个随机序列的种子,a,c,m是三个常数,不过这三个数不能随意选,m的大小决定随机数的周期,最大周期等于m,为了便于在计算机里实现,通常m选取$2^{32}$或$2^{64}$。在m已经确定为这两的时候,为了让周期尽可能大,常数a,c还至少要满足以下条件:
若 c 非 0,那么 c 与 m 互质;若 c 为 0,那么 a 与 m 互质 m 所有的素因子均能整除 a-1 若 m 是4的倍数,那么 a-1 也是 4 的倍数 a 和 c 都是正整数且小于 m 一些典型的常数取值可以参见wiki上的线性同余条目。
更高的需求 我们之所以使用线性同余,就是因为它实现简单,在对随机数质量要求较低的时候,例如用来作为treap的随机数,那么线性同余完全够用,但建议不要使用rand,因为在windows下不少编译器的最大值太小了,导致效果下降,自己写一个用参数a=69069,c=1,m=2^32比rand好,我在那篇关于treap的文章就是用了这组参数。线性同余法最大的缺陷是低位随机性特别差,如果使用类似next() % k的方式来获得区间在$[0,k-1]$的随机数,那么当线性同余迭代方程的m是2的幂且k也是2的幂的时候,灾难就发生了,特别地当k是2的时候,你将得到一个0101的循环序列。为了避免这种情况,通常会取线性同余结果的高位,而且低位去掉得越多,%2的周期就越长。例如结果是64位,取高32位作为最终结果,那么%2的周期就是 $2^{33}$ ,但这样会导致有效位减少,而且问题也没有根本地解决。另一种解决办法是选取一个素数作为m,例如2147483647正是一个素数,但如此一来,线性同余的计算速度就会慢不少,周期也没有前一个的长。两种基本实现如下:
KMP已经介绍过了,这次主要介绍的是Manacher
最长回文子串 Manacher算法要解决的问题就是求最长回文子串,用到的思维和扩展KMP实在是像,不过理解起来比扩展KMP简单。
先定义数组v,v[i]表示以第i个字符为中心,到回文串一端的距离,我们以字符串"acabacab"为例,如下表(index是下标)
string a c a b a c a b \0 index 0 1 2 3 4 5 6 7 8 m ^ i ^ e ^ v 1 2 1 4 i是当前要计算的指针,m是上次计算的指针,e是下一个要比较的位置的指针
然后++i,注意这时候,由于以字符b两边是对称的,所以在求v[4]的值的时候,可以先查v[2]的值,是1,所以v[4]也是1
string a c a b a c a b \0 index 0 1 2 3 4 5 6 7 8 m ^ i ^ e ^ v 1 2 1 4 1 再继续++i,同样的,在求v[5]的值的时候,可以先查v[1]的值,是2,但是,这个长度达到了e指针的位置,即i+2>=e,这时候就更新指针m,并扩展e的位置,即比较str[7]与str[3],找到以下标5为中心的回文串边界。
KMP之所以在竞赛中常见,并不是因为它用来匹配字符串,而是用它的next数组,为了介绍它,我们先讲讲最长公共前缀
最长公共前缀 我们拿字符串ababcab作为例子
string a b a b c a b len 0 0 1 2 0 1 2 这里所表达的是,例如取第3、4个字符”ab”,这个子串与前缀完全匹配,且它的长度是2,所以就记录2,而第3、4、5个字符”abc”与前缀不能完全匹配,就记作0,含义就这么简单,而且你会发现,计算b的时候,可以根据它所匹配的字符的偏移来,b如果是匹配的,就找到匹配的那个字符是数组中的第几个,它是第二个,所以填2进去。我们再来看更复杂的例子
string a b a c a b a b len 0 0 1 0 1 2 3 2 最后那个字符不匹配的时候,1是怎么计算出来的呢,直接重新计算当然也可以,但就出现重复计算了。我们考虑一下匹配过程,在前面的字符a的时候,前后各一个指针,像这样
string a b a c a b a b len 0 0 1 0 1 2 3 ? pointer ^ ^ 然后两个a匹配,arr[6] = pointer1 - arr 得到3,然后两指针一起移动
string a b a c a b a b len 0 0 1 0 1 2 3 ? pointer * ^ ^ 这时候,不匹配,那么前一个指针上一次指向的是arr[2]的位置,即图上*的地方,值是1,这个值如果是p,那就移动到arr[p]的地方,所以就移动到arr[1]的地方,本质上就是找到前一个匹配此后缀的位置,即
string a b a c a b a b len 0 0 1 0 1 2 3 2 pointer ^ ^ 然后再尝试匹配,这次匹配上了,然后前一指针指向第二个元素,所以赋值2