追記:速くなってませんでした!sorry https://x.com/yosupot/status/1689337328547016704?s=20
競技プログラミングではmod 261 - 1のローリングハッシュが安全性と速度のバランスが良く、広く使われています。 詳しくは https://qiita.com/keymoon/items/11fac5627672a6d6a9f6 などの記事が有用です。
このmod 261 - 1のmodintをより高速化することを試みます。先ほどの記事や、適当にライブラリを確認すると*1*2、乗算は以下の実装方法が広く使われています
using u64 = unsigned long long; using u128 = unsigned __int128; const u64 MOD = (1ULL << 61) - 1; u64 mul(u64 a, u64 b) { u128 t = (u128)(a) * b; t = (t >> 61) + (t & MOD); return (t >= MOD) ? t - MOD : t; }
ここで、値をそのままではなく8倍して管理することを考えます。こうするとif (t >= MOD)
相当の処理がoverflow checkになり、雰囲気的に良さそうな気がします。
u64 mul2(u64 a8, u64 b8) { u128 c = (u128)(a8) * b8; u64 x = (c >> 67 << 3), y = (c << 61 >> 64); u64 z; if (__builtin_uaddll_overflow(x, y, &z)) z -= MOD << 3; return z; } u64 mul(u64 a, u64 b) { u64 t = mul2(a * 8, b * 8) / 8; if (t == MOD) t = 0; return t; }
実際に確認してみましょう。
mul2のほうがめちゃくちゃすっきりしているのが確認できます。マジックナンバーがなく、本当に正しいのかこれという感じですが、読むと正しそうに思えます。
実際に O(N log2 N) SAを実装してみます
- before 4368ms: https://judge.yosupo.jp/submission/154054
- after 3692ms: https://judge.yosupo.jp/submission/154056
2割ほど早くなりました
注記
- まだあんまり使ってないので、バグってるかも
- そもそも従来のmodintのほうに改善の余地がありそう?tweet
- 値を[0, MOD]で管理する都合上比較が汚くなってしまう これなんとかなるのかな? -> https://twitter.com/noshi91/status/1688130780718092288