Goldman Sachs CodeSprint: Transaction Certificates

keywords
rolling hash | birthday attack

解法

ハッシュ値が重複するまでひたすら生成し続けるだけで高速に発見できることが知られている。これは誕生日攻撃と呼ばれている。nが小さい場合は重複値が存在しない可能性があるので、100以上になるまで大きくしておく。

#include <iostream>
#include <algorithm>
#include <map>
#include <vector>

using namespace std;

int main() {
  int n, k, p;
  unsigned long long m;
  cin >> n >> k >> p >> m;

  int u = 1;
  while (n * u < 100) {
    u++;
  }
  n *= u;

  auto gen = [&](int seed) {
    unsigned long long x = seed;
    vector<int> val;
    for (int i = 0; i < n; i++) {
      x ^= x << 13;
      x ^= x >> 7;
      x ^= x << 17;
      val.push_back(x % k + 1);
    }
    return val;
  };

  auto calc_hash = [&](vector<int> a) {
    long long hash = 0;
    for (long long x : a) {
      hash = (hash * p + x) & (m - 1);
    }
    return hash;
  };

  map<unsigned, int> mp;
  
  for (int i = 1;; i++) {
    long long hash = calc_hash(gen(i));
    if (mp.count(hash)) {
      for (long long x : gen(i)) {
        cout << x << ' ';
      }
      cout << endl;
      for (long long x : gen(mp[hash])) {
        cout << x << ' ';
      }
      return 0;
    } else {
      mp[hash] = i;
    }
  }
}

二冪なので他にも攻略法があって、それはhos氏のブログが参考になる。

文字列 \(110000\) と文字列 \(000011\) の rolling hash はそれぞれ

\begin{equation}
1x^5+1x^4+0x^3+0x^2+0x^1+0 \\
0x^5+0x^4+0x^3+0x^2+1x^1+1
\end{equation}

であり、その差は \(x^5+x^4-x-1\) である。この多項式は \(x\) が奇数のとき常に \(32\) の倍数になる。つまり法を \(32\)、基数を奇数としたとき必ず衝突する。法が二冪であればこのような多項式が容易に構成できる。

\(v(n)\) を \(2\) が \(n\) を割り切る回数とし、多項式 \(f\) に対して

\begin{equation}
V(f)=\min_{n \in 2\mathbb{Z}+1}v(f(n))
\end{equation}

とする。\(V(f) \ge 32\) となる多項式 \(f\) を見つければ良い。\(V(x^{2k} - 1) = V( (x^k - 1) (x^k+1) ) \ge 1+V(x^k - 1)\) なので、

\begin{equation}
V( (x-1)(x^2-1)(x^4-1)\cdots(x^{128}-1) ) \ge 32
\end{equation}

である。これは展開時の係数がすべて \(\pm 1\) であり、さらにその係数は組合せ的に求まる。

n, k, p, m = map(int, input().split())
# (x-1)(x^2-1)(x^4-1)(x^8-1)(x^16-1)(x^32-1)(x^64-1)(x^128-1)
#  1    2      3      4      5       6       7      8        >32
n = (256+n-1)//n*n
ans = [0]*(n-256) + [(-1)**bin(i).count('1') for i in range(256)]

mp1 = {0: 1, 1: 2, -1: 1}
mp2 = {0: 1, 1: 1, -1: 2}
print(*[mp1[v] for v in ans])
print(*[mp2[v] for v in ans])

補足

  • よく考えると基数が偶数 \((p=2)\) のときバグる。
  • \(x\) が奇数のとき \(x^2+1\) は \(4k+2\) 型の整数になるので、\(v(x^2+1)=1\) である。これにより厳密に \( v(x^{2^k} - 1)=v(x^2-1)+k \) である。この結果は LTE補題に似ているが、LTE補題は偶素数と奇素数で振る舞いがわずかに異なることに注意。
  • 対二冪の多項式の係数列を Thue-Morse sequence というらしいですね。