XorShift

更新日志 2025/04/19:开工。

2025/04/21:更正冲突率证明。提供树哈希经验


概念

XorShift 是一种伪随机数生成算法,有 \(2^{32}-1,2^{64}-1\) 的周期,以及更大的 \(2^{128}-1\) 等超大周期。

我们可以利用其周期长的特点进行字符串哈希、树哈希等哈希操作。

本文除了提供模板外,将重点对字符串冲突率进行分析,并提供一点树哈希经验。

实现

本质上是巧妙的数学算法,详见原论文

形式上表现为:

v^=v<<a;
v^=v>>b;
v^=v<<c;

不同的取值有不同的效果,原论文中罗列了所有 \(2^{32}-1\)\(2^{64}-1\) 周期的可用 \(a,b,c\) 取值。

一般来说,我们用不到更大的周期。如果你需要的话,可以去原论文学习。

模板

这里使用我个人喜欢的 \(a,b,c\) 取值。

ull shift(ull x){
	x^=x<<5;
	x^=x>>11;
	x^=x<<54;
	return x;
}

使用六十四位显然通常更优,所以不提供三十二位算法了。

如果要用三十二位的话,我认为 \(1,3,10\) 这组数据比较好记。

优化

一道题目中可能的数据集通常是极大的,比如字符集大小为 \(2\) 时,长度为 \(64\) 的字符串就占满了所有可能的哈希值。我想说的是任何哈希都必然可以被对着卡掉。

因此我们可以来一点加密:

ull mask=mt19937_64(time(0))();
ull shift(ull x){
	x^=mask;
	x^=x<<5;
	x^=x>>11;
	x^=x<<54;
	x^=mask;
	return x;
}

对于 \(mask\) 的异或并不会影响周期长度,但是顺序被随机打乱了。

你可以理解为我们在里层前进,但每次返回的都是表层的值,而我们直接对这个表层的值进行状态转移,每次跃迁到的新的位置就很随机了。同时,表里是一一对应的。

字符串哈希冲突率计算

首先,周期长度长并不代表哈希转移周期长,你可以认为我们每一次转移,都是在这个周期上随机迁跃。因此我们需要计算冲突率。

这里我们对字符串哈希冲突率进行粗略计算。

hsh[i]=shift(hsh[i-1])^now;

通常这个状态只会影响最低几位。也就是说,迁跃前后,前面的所有位都是相同的而只有最后几位产生变化。

在后面,我使用“位移哈希”及相近的词汇代表 shift 操作,而用迁跃表示 ^now 操作。

迁跃前后前面所有位都相同,那么我们把迁跃不影响的的前面几位相同的数视作一个等价类,那么一个等价类的大小其实就只有 \(now\) 的值集的大小。

对于一个首次冲突,我们考虑其最后一次转移,也就是两个不同的哈希值经过不同的转移状态变成了一个相同的哈希值。

首先,迁跃之前等价类不同的两个哈希值迁跃之后不会相等。

然后,位移之前等价类相同的两个不同哈希值位移之后不会相等。

也就是说,出现首次冲突的两个哈希值,必要条件是在同一等价类中,那么必然是从不同的等价类经过一次位移到达了同一个等价类。

插入一个新的字符串时,发生这种情况的概率的概率是 \(k\frac{|E|}{C}\)\(C\) 为周期长度,\(k\) 为当前存在的不同哈希值数,\(|E|\) 表示等价类大小,字符集大小为 \(s\)

然后,出现冲突还需要满足位移之后变为同一个数,这个情况的概率是 \(\frac{1}{s}\)

假如共有 \(n\) 种不同种类的字符串,那么整个哈希程序中,出现冲突的概率就大致是:

\[\sum_{i=1}^n(i-1)\frac{|E|}{Cs}=\frac{n(n-1)|E|}{2Cs} \]

我们粗略地认为 \(|E|=s\),那么冲突率就是:

\[\frac{n(n-1)}{2C} \]

这个公式是不准确的,我们假设当前所有哈希值在周期中均匀分布。因为 xorshift 本身就是随机数生成算法,因此我们可以相信这一点。事实上也许这里需要更精确的证明,但我暂时没有足够的精力和智力去搞它。

如果你把 \(n=10^7\) 代入这个公式,那么结果也不会非常炸裂。但为了稍微精确一些,你需要区分不同字符串数总字符数的区别。

具体的说,如果你希望再字符总数一定的情况下最大化不同字符串数,那么:

  • 每个字符串尽量不同
  • 从小到大生成字符串

假如我们最后一个可以完整生成的字符串长度为 \(l\),那么:

  • 不同的字符串数为 \(\sum_{i=1}^ls^i\)
  • 总字符数为 \(\sum_{i=1}^lis^i\le n=10^7\)

\(s=26\)\(l=5\) 时总字符数就已经到达了 \(61288890\)。经过计算,\(n=10^7\) 时 不同的字符串数只有 \(3505608\) 种。

我们使用 \(2^{64}\) 的周期算出来冲突率大概是 \(3\times10^{-7}\)

树哈希经验

众所周知,我们可以使用它来进行树哈希,我目前尚未进行冲突率计算,仅能提供在UOJ上的一些经历。

首先,我们的思路是 \(hsh_i=\sum_{j\in\mathrm{son}(i)}\mathrm{shift}(hsh_j)+c\)。采用 \(64\) 位周期。或者说,见 OI Wiki

\(c>0\),因为 \(\mathrm{shift}(0)=0\),就伪了。

如果我们令 \(c=1\) 并不采取 \(mask\) 优化,是 WA 的。

采取 \(mask\) 优化,AC

\(c\) 令为一个较大数,AC

在同一个周期用两个不同 \(c\) 双哈希,大数 AC,小数 WA

我们数学归纳一下,令 \(c\) 为一个大数,并采取 \(mask\) 优化,就很好了。如果常数允许,还可以采取多哈希。

终极版本:AC

终极版本关键代码
int n;
vec<int> g[N];

auto rnd=mt19937_64(time(0));
ull mask1=rnd(),mask2=rnd();
ull shift(ull x,ull mask){
    x^=mask;
    x^=x<<5;
    x^=x>>11;
    x^=x<<54;
    x^=mask;
    return x;
}

pair<ull,ull> hsh[N];
set<pair<ull,ull>> hshs;

void dfs(int now,int fid){
    hsh[now]={2839583728475643,1827465938276543};
    for(auto nxt:g[now]){
        if(nxt==fid)continue;
        dfs(nxt,now);
        hsh[now].fir+=shift(hsh[nxt].fir,mask1);
        hsh[now].sec+=shift(hsh[nxt].sec,mask2);
    }
    hshs.insert(hsh[now]);
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
    repl(i,1,n){
        int a,b;cin>>a>>b;
        g[a].pub(b);g[b].pub(a);
    }
    dfs(1,0);
    cout<<hshs.size();
    return 0;
}
posted @ 2025-04-19 14:46  LastKismet  阅读(463)  评论(0)    收藏  举报