ABC286 F問題 Guess The Number 2

atcoder.jp

どんな操作が行われるか具体例で考える

ジャッジ側の操作を具体的に考えてみます。

 A = \left( 2,3,4,4 \right) を渡すとします。つまり、 f(1) = 2, f(2) = 3,  f(3) = 4, f(4) = 4 です。
N=1 の場合、 B= \left( 2,3,4,4 \right) と Aと同じ数字が返ってきます。
N=2 の場合、
 f^2(1) = f(2)=3
 f^2(2) = f(3)=4
  f^2(3) = f(4)=4
 f^2(4) = f(4)=4
より  B= \left( 3,4,4,4 \right) が返ってきます。
N=3 の場合、
 f^3(1)=f^2(2) = f(3)=4
 f^3(2)=f^2(3) = f(4)=4
  f^3(3)=f^2(4) = f(4)=4
 f^3(4)=f^2(4) = f(4)=4
より  B= \left( 4,4,4,4 \right) が返ってきます。
N=3 より大きな N については、  f(4)=4 と値が 4 のまま変わらないため、返ってきた B からは Nが 1か2か3以上 のいずれかということしかわかりません。

そこで、 f(4) を別の数字に変えてみます。
例えば  A = \left( 2,3,4,2 \right) とします。
このとき、
N=1 の場合、 B=A= \left( 2,3,4,2 \right)
N=2 の場合、
 f^2(1) = f(2)=3
 f^2(2) = f(3)=4
  f^2(3) = f(4)=2
 f^2(4) = f(2)=3
N=3 の場合、
 f^3(1)=f^2(2) = f(3)=4
 f^3(2)=f^2(3) = f(4)=2
  f^3(3)=f^2(4) = f(2)=3
 f^3(4)=f^2(2) = f(3)=4
N=4 の場合、
 f^4(1)=f^3(2)=f^2(3) = f(4)=2
 f^4(2)=f^3(3)=f^2(4) = f(2)=3
  f^4(3)=f^3(4)=f^2(2) = f(3)=4
  f^4(4)=f^3(2)=f^2(3) = f(4)=2
となり、N=1 と N=4 が同じになりました。2~4番目の数字に注目すると、 (3,4,2) \to (4,2,3) \to (2,3,4) \to (3,4,2) \to \cdots と周期3で巡回しています。
そのため、返ってきた B からは N を 3で割った余りがわかります。(先頭の数字から一番小さな数字の2を引いた値が余りになっています。)
一般に L 個の数字で周期 N の巡回をするような数字の並びを作ることができて、それによって N を L で割った余りがわかります。

中国剰余定理

余りから元の数が特定できるかどうかは、中国剰余定理からわかります。

与えられた  k 個の整数  m_1, m_2, \cdots, m_k がどの二つも互いに素ならば、任意に与えられる整数  a_1, a_2, \cdots, a_k に対し

 x ≡ a_1 (\mod m_1),
 x ≡ a_2 (\mod m_2),
   …
 x ≡ a_k (\mod m_k)
を満たす整数 x が  m_1m_2…m_k を法として一意的に存在する。

まず、  m_1, m_2, \cdots, m_k素数であるとします。 周期  m_i とするためには、  m_i 個の数字が必要になります。使える数字は最大110 という制限があるため、
 2+3+5+7+11+13+17+19+23 = 100 までになります。
この場合、中国剰余定理より  2\times3\times5\times7\times11\times13\times17\times19\times23 \approx 2.2\times 10^8 までの値であれば一意に決まります。ところが Nは最大 10^9 なので、これではNが一意に決まりません。
そこで、さらに工夫が必要になります。
素数に限定しないで、 2の代わりに 2^2=4 , 3の代わりに 3^2=9 を使ってみましょう。すると長さは  4+9+5+7+11+13+17+19+23 = 108 で 最大110 の制限を満足し、 4\times9\times5\times7\times11\times13\times17\times19\times23 \approx 1.3\times 10^9 までの値が一意に決まります。

計算法

Nを  m_i で割った余りが  r_i,
 m_j で割った余りが  r_j であるとします。(  m_i m_j は互いに素)
 N = p  m_i + r_i=q m_j + r_j と書けます。
  p  m_i + r_i= r_j (\mod m_j)
ここに、  a m_i   ≡ 1 (\mod m_j) となる  a (m_i の逆数) をかけると、
  p = a (r_j-r_i) (\mod m_j)
これより、 p = s m_j+a (r_j-r_i) と書けます。
 N = (s m_j+a (r_j-r_i) )  m_i + r_i = s m_i m_j+a m_i (r_j-r_i) + r_i
つまり、N を  m_i m_j で割った余りが  a m_i (r_j-r_i) + r_i になります。
これを繰り返していけば、  m_1m_2…m_k で割った余りが求められます。

コード例 (Julia)

function solve()

  # ジャッジに渡す配列の準備

  P = [4, 9, 5, 7, 11, 13, 17, 19, 23]
  N = length(P)
  L = sum(P)

  A = Vector(1:L)    # A = [1,2,3,...,L]

  base = 0
  for i = 1:N
    # 周期 P[i] の巡回をするように、数字をずらす
    a = base+1
    b = base+P[i]
    A[a:b] = circshift(A[a:b],-1)
    base += P[i]
  end

  # 配列をジャッジに渡す

  println(L)
  for i = 1:L-1
    print(A[i]," ")
  end
  println(A[L])

  # ジャッジから返される配列を受け取る
  readInts() =  parse.(Int,split(readline()))
  B = readInts()

  # 余り
  r = Vector{Int}(undef,N)
  base = 0
  for i = 1:N
    # 先頭の数字から、巡回する中で一番小さな数字を引く
    a = base+1
    r[i] = B[a] - a
    base += P[i]
  end

  # N を求める
  m = P[1]
  ans = r[1]
  for i = 2:N
    a = invmod(m,P[i])
    ans = a * m * (r[i] - ans) + ans
    m *= P[i]
    ans %= m
  end

  if ans < 0
    ans += m
  end
  println(ans)

  return 0;

end  # function solve


# main
solve()