非再帰で拡張ユークリッドの互除法を書く

n 番煎じだと思いますが,既存の説明で私はあまりよく理解できていなかったので刺さる人がいればいいなと思って書きました.

問題

整数  a,  b が与えられる. a b の最大公約数  g=\gcd(a, b) ax+by=g を満たす整数  x,  y を求めよ.

再帰解法

 a b で割ったときの商と余りをそれぞれ  p,  q とおく.このとき  a=bp+q であり,方程式に代入して (1) を得る.

 \displaystyle
\begin{align}
ax+by&=g\\
(bp+q)x+by&=g\\
b(px+y)+qx&=g\tag{1}
\end{align}

 \gcd(b,q)=\gcd(a,b) なので, a\leftarrow b,  b\leftarrow q として再帰的にこの問題を解けば元の解  (x,\ y) が復元できます.具体的には, bx'+qy'=g を満たす整数  x',  y' に対し, x=y',  y=x'-py' とすればよいです.

再帰の終端条件は  b=0 であり,このとき  (x,\ y,\ g)=(1,\ 0,\ g) とすればよいです.また,再帰の深さは  O(\log\min(a,b)) であることが示されます.この辺りの説明は qiita.com が詳しいので分らない場合は参照してください.

再帰解法

上の再帰解法において,再帰の深さが  i のときに解く方程式を  a_i x+b_i y=g,解を  (x,\ y)=(x_i,\ y_i) とします.また, a_i=b_i p_i + q_i とします. b _ i\neq 0 のとき,上述の解の復元方法から,次の漸化式が立ちます.

 \displaystyle
\begin{cases}
x_i=y_{i+1}\\
y_i=x_{i+1}-p_i y_{i+1}
\end{cases}

また,再帰の終端の深さを  n とすると, x_n=1,  y_n=0 です.よくある漸化式と異なり,添え字の大きい方から  (x_i,\ y_i) 求まっていくことに注意です.一方で, a_i,  b_i は次のように添え字の小さい方から求まっていきます.このことが非再帰で拡張ユークリッドの互除法を書くことを難しくしています.

 \displaystyle
\begin{cases}
a_i=b_{i-1}\\
b_i=q_{i-1}
\end{cases}

ここで, (x_i,\ y_i) に対する遷移を行列で表現すると以下のようになります.

 \displaystyle
\begin{pmatrix}
0&1\\
1&-p_i
\end{pmatrix}
\begin{pmatrix}
x_{i+1}\\
y_{i+1}
\end{pmatrix}=
\begin{pmatrix}
x_i\\
y_i
\end{pmatrix}

これを  i=0,\ldots,n-1 に対して適用することで,次の  (2) を得ます.

 \displaystyle
\begin{pmatrix}
0&1\\
1&-p_0
\end{pmatrix}
\begin{pmatrix}
0&1\\
1&-p_1
\end{pmatrix}\cdots
\begin{pmatrix}
0&1\\
1&-p_{n-1}
\end{pmatrix}
\begin{pmatrix}
x_n\\
y_n
\end{pmatrix}
=
\begin{pmatrix}
x_0\\
y_0
\end{pmatrix} \tag{2}

 a_0=a,  b_0=b であり, x_0,  y_0 ax+by=g を満たす整数解であること,加えて  x_n=1,  y_n=0 であることに注意します.

 a_i,  b_i,  p_i は添え字の小さい順に求まっていくので,行列を左から順にかけていく *1 ことで,めでたく非再帰 (x,\ y) が求められます.

実装

C++17 で動くと思います.バグってたらすみません.

利便性のため求まる  \gcd が非負になるように補正をかけていますが,必須ではありません.

// return {{x,y},g} s.t. ax+by=g=gcd(a,b)>=0. 
std::pair<std::pair<long long, long long>, long long> ext_gcd(long long a, long long b) {
    /** 計算中の行列を
     *   x y
     *   z w
     *  としている
     */
    long long x = 1, y = 0;
    long long z = 0, w = 1;
    long long tmp;
    while (b) {
        long long p = a / b, q = a % b;
        tmp = x - y * p; x = y; y = tmp;
        tmp = z - w * p; z = w; w = tmp;
        a = b; b = q;
    }
    // gcd(a,b)>=0 となるように補正を掛ける (任意)
    if (a >= 0) return {{x, z}, a};
    else return {{-x, -z}, -a};
}

*1: 行列の乗算は結合則を満たすのでこのようなことが出来ます

AtCoder Beginner Contest 197(Sponsored by Panasonic)

A-E は Python,F は Java

A - Rotate

Python が楽.

S = input()
print(S[1:] + S[0])

B - Visibility

 (X, Y) を中心にして,4 方向それぞれに対して壁にぶつかるまで進む. (X,Y) を重複してカウントしないように注意.

あんまりきれいに実装できなかった.

H, W, X, Y = map(int, input().split())
X -= 1
Y -= 1
S = []
for _ in range(H):
    S.append(input())
ans = 1
for j in range(Y + 1, W):
    if S[X][j] == '#':
        break
    ans += 1
for j in reversed(range(Y)):
    if S[X][j] == '#':
        break
    ans += 1
for i in range(X + 1, H):
    if S[i][Y] == '#':
        break
    ans += 1
for i in reversed(range(X)):
    if S[i][Y] == '#':
        break
    ans += 1
print(ans)

C - ORXOR

数列の各項の隙間  N-1 個に対して,それぞれ切る or 切らないの  2 ^ {N-1} 全探索.区間列挙の際は両端に番兵を入れておくとよい.

N = int(input())
A = list(map(int, input().split()))

ans = 1 << 32
for i in range(1 << N - 1):
    sep = [0] + [j + 1 for j in range(N - 1) if i >> j & 1] + [N]
    K = len(sep) - 1
    x = 0
    for l, r in zip(sep, sep[1:]):
        v = 0
        for t in A[l:r]:
            v |= t
        x ^= v
    ans = min(ans, x)

print(ans)

D - Opposite

点の回転がしたいので複素数を使う.

 K=N/2,\ z _ 0 = x _ 0 + i\cdot y _ 0,\  z _ K = x _ K + i\cdot y _ K とおく.正  N 角形の中心を  z _ M とすると, z _ M = (z _ 0 + z _ K) / 2 である.そして, z _ M を用いて目標の点  z _ 1 は次のように表せる.

 \displaystyle
z_1=z_M+(z_0-z_M)\left(\cos \frac{\pi}{K}+i\cdot\sin\frac{\pi}{K}\right)

実装においては,Python では標準で complex 型がサポートされているのでこれを使うとよい.

import math

N = int(input())
K = N // 2
x0, y0 = map(int, input().split())
xK, yK = map(int, input().split())

z0 = complex(x0, y0)
zM = complex((x0 + xK) / 2, (y0 + yK) / 2)

rot = complex(math.cos(math.pi / K), math.sin(math.pi / K))

z1 = (z0 - zM) * rot + zM
print(z1.real, z1.imag)

E - Traveler

DP をやるだけなんだけど,何を思ったか貪欲方針があると思って時間を溶かした.なんで...?

ボールが一つ以上存在するような全ての色に対して,その昇順に番号を  1,\ldots,M と振りなおす.そして,各色の玉の左端の位置  L _ i と右端の位置  R _ i を計算しておく.

ここで,以降の実装の簡単のため,色  0 をスタート地点に対応させ,色  M+1 をゴール地点に対応させる.このとき, L _ 0 = R _ 0 = L _ {M + 1} = R _ {M + 1} = 0 である.

2 つの DP テーブル  \mathrm{dpL} および  \mathrm{dpR} を,それぞれ以下のように定義する.

 \displaystyle
\mathrm{dpL[i]}=\text{色 $i$ の玉を回収し終えて $L _ i$ にいるような場合における,それまでの移動距離の最小値}
 \displaystyle
\mathrm{dpR[i]}=\text{色 $i$ の玉を回収し終えて $R _ i$ にいるような場合における,それまでの移動距離の最小値}

まず, i=0 の場合は明らかに  \mathrm{dpL}[0]=\mathrm{dpR}[0]=0 である.

 i\gt 0 の場合は,次のように遷移を書くことが出来る.

 \displaystyle
\mathrm{dpL[i]}=\min(
\mathrm{dpL[i-1]}+|L_{i-1}-R_i|,
\mathrm{dpR[i-1]}+|R_{i-1}-R_i|)+R_i-L_i
 \displaystyle
\mathrm{dpR[i]}=\min(
\mathrm{dpL[i-1]}+|L_{i-1}-L_i|,
\mathrm{dpR[i-1]}+|R_{i-1}-L_i|)+R_i-L_i

求める答えは  \mathrm{dpL}[M+1] または  \mathrm{dpR}[M+1] である.

F - Construct a Palindrome

難しい,みんな解くのが速い...

回文の中心から外に向かってパスを伸ばしていくイメージで,次のテーブルを埋めていく.

 \displaystyle
\mathrm{dist}[u][v]=\text{回文となる $u$, $v$ パスの最短長}

初め,すべての  0\leq u,v\lt N に対して  \mathrm{dist}[u][v]=\infty と初期化しておく.

自明なケースとして  \mathrm{dist}[i][i]=0\;(i=0,\ldots,N-1) である.また, \mathrm{dist}[A _ j][B _ j]=\mathrm{dist}[B _ j][A _ j]=1\;(j=0,\ldots,M-1,\ A _ j \neq B _ j) である ( A _ j = B _ j の場合は最短長は  0 であるため除く必要がある).

以上を基底ケースとして,テーブルの残りの部分を幅優先探索により埋めることができる.

具体的な手続きを以下に示す.なお,以降は幅優先探索に用いるキューを  q,頂点  u を始点とする辺の集合を  E(u) と表すことにする.また, u を始点, v を終点とする文字  c が書かれた辺を  (u, v, c) と三つ組にして表現する.

  1. 基底ケースの頂点組  (u,v) q に push する.
  2.  q が空なら,全ての  \mathrm{dist} を確定済みとして終了する.空でないなら,3 に進む.
  3.  q の先頭要素  (u, v) を pop する *1 e _ u=(u, x, c _ u)\in E(u), e _ v=(v, y, c _ v)\in E(v) の組を全探索する.このとき,各  e _ u, e _ v に対して,次のような場合分けを行う.全探索を終えたら,2 へ戻る.
    1.  c _ u \neq c _ v のとき:
      何もしない
    2.  c _ u = c _ v かつ  \mathrm{dist}[x][y] が未確定のとき:
       \mathrm{dist}[x][y]=\mathrm{dist}[y][x]=\mathrm{dist}[u][v]+2 確定し *2 q (x,y) を push する.
    3.  c _ u = c _ v かつ  \mathrm{dist}[x][y] が確定済みのとき:
      何もしない

さて,この手続きにおける遷移の数が膨大になりそうだが,実は  O(M ^ 2) となっている.これは,各頂点組  (u, v) に対して接続する辺を全探索を行うのは一回きりであり,遷移の数は次のように計算できることから従う.

 \displaystyle
\sum _ {0\leq u, v\lt N}|E(u)||E(v)|=\sum_{u=0}^{N-1}|E(u)|\sum_{v=0}^{N-1}|E(v)|=M^2

問題の答えとしては, \mathrm{dist}[0][N-1] または  \mathrm{dist}[N-1][0]の値を参照すればよい.( \infty なら -1 を出力)

*1:このとき, \mathrm{dist}[u][v] の値は既に確定していることに注意する

*2:一般に,辺のコストが全て等しい場合の多始点最短経路問題は幅優先探索で解くことが出来る.

AtCoder Regular Contest 110 F - Esoswap

面白い解法で解けたので (流石に既出かもしれない)

解法

これだけです.何をしているかというと,数列の後ろから順番に同じ場所を  N-1 回ずつ連続して選択しています.

順列  P を読まなくても解けるのが個人的に面白いと思った部分です.

N = int(input())
print(N * (N - 1))
for i in range(N):
    for _ in range(N - 1):
        print(N - i - 1)

提出すると確かに AC を取れますが,本当に正しいのでしょうか?

正当性

まず,添え字  k を連続して選択したときの  P _ k の値の変化を考えます.

少し実験すると,

 
\begin{aligned}
(\star):\text{「}
&\text{添え字 $k$ を連続して選択したとき,$P _ k = 0$ となるまでは毎回異なる値が現れ,}\\
&\text{$P _ k = 0$ になってからは変化しない」}
\end{aligned}

という性質がありそうだと気づきます.

 P _ k = x のとき,添え字  k を選択すると  x の位置は  k + x \bmod N へと移ります.従って,再び  P _ k = x となるためには, P _ k = y\in\{0,\ldots,N-1\}\;\mathrm{s.t.}\;k + y \equiv k + x となる必要があります.そして,これを満たすのは  y=x に限られます.つまり,2 回以上同じ値となるのは  x=0 しかありえないということになり, (\star) は正しいです.

次に,外側のループが一つ進むごとに順列がどう変化しているかを見てみます.以下,例として  P=(0, 2, 3, 5, 4, 1) を用います.

N = 6
P = [0, 2, 3, 5, 4, 1]

def swap(i):
    j = (i + P[i]) % N
    P[i], P[j] = P[j], P[i]

for i in range(N):
    for _ in range(N - 1):
        swap(N - i - 1)
    print(P)

外側のループ毎に順列の状態を出力するコードです.以下に出力を示します.

[1, 2, 3, 5, 4, 0]
[2, 3, 4, 5, 0, 1]
[3, 4, 5, 0, 1, 2]
[4, 5, 0, 1, 2, 3]
[5, 0, 1, 2, 3, 4]
[0, 1, 2, 3, 4, 5]

 i 回目のループが終了した時点で,P _ {N-i-1+j} = j\;(j=0,\ldots,i) となっていそうです.これを以下の 2 ステップに分けて示します.

  •  i=0 の場合に正しいこと
  •  i=l-1\geq 0 の場合に正しいと仮定すると, i=l の場合も正しいこと

 i=0 の場合は, (\star) より添え字  k=N-1 N-1 回連続して選択した後は必ず  P _ k = 0 となっていることから従います.

 i=l-1\geq 0 の場合に正しいことを仮定すると,P _ {N-l+j} = j\;(j=0,\ldots,l-1) です.

添え字  k=N-l-1 を連続して選択した場合に,swap 相手の添え字が初めて  k より大きくなるタイミングを考えます.仮定より,P _ {m} \in \{0,\ldots,l-1\} を満たす全ての  m に対して  k\lt m が成立します.従って,これは  P _ k = l となったタイミングに限られます.そして,

  •  P _ k = l となるまでは  P _ k \lt l となりえないこと
  •  (\star) の性質

の 2 つから, P _ k = l となるまでの最大の操作回数は  N-l-1 回で上から抑えることが出来ます.更に, P _ k = l となった直後の  l 回の操作で  P _ {N-l-1+j} = j\;(j=0,\ldots,l) となることから, i=l の場合も正しいことが言えます.

以上より,冒頭に示した解法は正当であることが言えました.

AtCoder Beginner Contest 196

初めて PythonJava の二刀流で参加した.A-E は Python で解いて,F は TL の関係で Java で解いた.

A - Difference Max

一瞬場合分け問題かと思ったけど,冷静になると  x=b,y=c が最適で,答えは  b-c.これはどっちの言語でもそんなに変わらなさそう.

_, b = map(int, input().split())
c, _ = map(int, input().split())
print(b - c)

B - Round Down

これは in とスライスを気軽に使える Python が楽.

X = input()
if '.' in X:
    X = X[:X.index('.')]
print(X)

C - Doubled

 N は最大で  10 ^ {12} にもなるので,全探索は間に合わない.そこで,前半分だけを全探索することにすれば, \sqrt{N} 通り程度で済むので間に合う.

実装上は  \sqrt{N} ではなく  10 ^ 6 を上限に取ってしまう方が楽?累乗を前計算したけど,文字列に変換してつなげて整数に戻す方が良かったかも.

N = int(input())
p = [1]
for i in range(10):
    p.append(p[-1] * 10)
A = 0
for i in range(1, 1000001):
    l = len(str(i))
    x = i * p[l] + i
    if x <= N:
        A += 1
print(A)

D - Hanjo

 HW\leq 16 と制約が小さいので,既に埋まった畳の集合  S を状態に持つ DP をベースに考える.あとは  1\times 2 の畳の枚数  a 1\times 1 の畳の枚数  b も持てば,めでたく遷移が書ける.

制約より  b S a から一意に定まるので状態として持つ必要はないが,(メモ化再帰の場合は) 持っても探索空間は増えないので持っておくと多少楽が出来る.

マス  (i, j) に整数  iW + j を割り当て,まだ畳が敷かれていない最小の整数に対応するマスを含むように畳を置いていくことで重複なく数え上げることが出来る.

H, W, A, B = map(int, input().split())
N = H * W
D = {}

def dfs(s, a, b):
    if s == (1 << N) - 1:
        return 1
    if (s, a, b) in D:
        return D[(s, a, b)]
    res = 0
    for i in range(N):
        if (s >> i) & 1 == 0:
            if a:
                r, c = divmod(i, W)
                if c < W - 1 and (s >> i + 1) & 1 == 0:
                    res += dfs(s | 1 << i | 1 << i + 1, a - 1, b)
                if r < H - 1 and (s >> i + W) & 1 == 0:
                    res += dfs(s | 1 << i | 1 << i + W, a - 1, b)
            if b:
                res += dfs(s | 1 << i, a, b - 1)
            break
    D[(s, a, b)] = res
    return res

print(dfs(0, A, B))

E - Filters

難しくない?20 分近く掛かった.

関数の形を観察すると, f_i(\cdots (f_2(f_1(x)))\cdots) は図 E に示すような形 になっていることが分かる.

f:id:suisen_kyopro:20210321005243p:plain
図 E

従って,図中の  L,R,U,D をうまく更新できればクエリに高速に答えることが出来る.

初め, L=D=-\infty,R=U=+\infty としておく.そして, t_i の値によって  L,R,U,D は以下のように更新することが出来る.

  •  t_i=1 のとき:  D\leftarrow D+a _ i,U\leftarrow U+a _ i と更新
  •  t_i=2 のとき:  D の変化量に注目すると,これは  \max (0,a _ i-D) である. L も同じだけ変化するので, D\leftarrow D+d,L\leftarrow L+d と更新できる.注意しないといけないのは, a _ i \gt U である場合は  U,R も更新されるという点である.これは, U\leftarrow \max (U,D), R\leftarrow \max(R,L) とすればよい.ここで, D,L は更新後の値である.
  •  t_i=3 のとき:  t_i=2 のときと同様に考えればよい.
N = int(input())
INF = 10 ** 18
D, U = -INF, +INF
L, R = -INF, +INF
for _ in range(N):
    a, t = map(int, input().split())
    if t == 1:
        D += a
        U += a
    elif t == 2:
        d = max(0, a - D)
        D += d
        L += d
        R = max(L, R)
        U = max(U, D)
    elif t == 3:
        d = max(0, U - a)
        U -= d
        R -= d
        L = min(L, R)
        D = min(U, D)

input()
for x in map(int, input().split()):
    if x <= L:
        print(D)
    elif x >= R:
        print(U)
    else:
        print(D + (x - L))

F - Substring 2

問題の F.bitset による 64 倍高速化で通せそうな雰囲気を感じたので,その方針を採用した (コンテスト後に分かったが,これは落とされるべき解法だったらしい).

以後, S[l:r] S l 文字目から  r-1 文字目までの部分文字列を表すものとする.

 N=|S|,M=|T| とする. S および  T を二進数として解釈すれば,\displaystyle \min _ {0\leq i\leq N-M}\{\mathrm{popcount}(S[i:i+M]\oplus T)\} を求める問題と言い換えられる.ここで, \oplus はビット毎の排他的論理和 \mathrm{popcount}(x) は整数  x の二進数表記における  1 の個数である.

 \mathrm{popcount}(S[i:i+M]\oplus T) を求める際に, S および  T を 64 bit 毎に区切って 64 bit 整数の配列に押し込むことで排他的論理和および popcount の計算の回数を削減することを考える.

 S[i:i+M] T の間で,64 bit 整数の境目が完全にかみ合っている場合は演算の回数は 1/64 となりかなりの高速化が期待できる.

しかし,当然うまくかみ合わない場合も存在する.この場合は各整数を 2 分割して比較する必要があるため,理想ケースに比べて 2 倍遅くなってしまう.

文章で書くと分かりにくいので,図を示す.1/64 となるケースが図 F-1 であり,1/32 となるケースが図 F-2 である.

f:id:suisen_kyopro:20210321012849p:plain
図 F-1. 境目が一致する理想ケース

f:id:suisen_kyopro:20210321013220p:plain
F-2. 境目が一致しないケース

図から分かるように,演算回数が 1/64 となるケースは全ての  i のうち 1/64 程度しか存在しない.これでも愚直な実装に比べて 32 倍程度の高速化は期待できるが,この問題においては不十分であり TL には間に合わない.

そこで,さらなる工夫を考える.各  S[0:N],S[1:N],\ldots,S[63:N] に対して 64 bit 毎の分割を行い,配列を作成しておく.これにより, i の値によって適切な配列を選択すれば,必ず理想ケースに落とし込むことが出来る.具体的には, S[i\bmod 64:N] に対応する配列を選択するようにすればよい.

実装においては,最後の端数の処理がやや面倒であるが,本質部分とは言えないので説明を省略する.詳しくは以下の Java による実装を参照.

String s = sc.next();
String t = sc.next();
int n = s.length();
int m = t.length();

// 開始位置の mod 64 の値ごとに 64 bit 分割を行う.
long[][] x = new long[64][];
for (int i = 0; i < 64; i++) {
    // 切り上げ除算
    int p = (n - i + 63) / 64;
    long[] r = x[i] = new long[p];
    for (int j = 0; j < p; j++) {
        // Long.parseLong を使用した場合,64 bit 目が 1 だと実行時エラーとなる.
        r[j] = Long.parseUnsignedLong(s, i + (j << 6), Math.min(n, i + (j + 1 << 6)), 2);
    }
}
// 切り上げ除算
int p = (m + 63) / 64;
long[] y = new long[p];
for (int j = 0; j < p; j++) {
    y[j] = Long.parseUnsignedLong(t, j << 6, Math.min(m, j + 1 << 6), 2);
}

int ans = m;
// 開始位置を全探索
for (int i = 0; i + m <= n; i++) {
    int k = i >> 6;
    int r = i & 63;
    long[] a = x[r];
    int sum = 0;
    for (int j = 0; j < p - 1; j++) {
        sum += Long.bitCount(a[k + j] ^ y[j]);
    }
    // 末項の帳尻合わせ
    int l1 = k + p == a.length ? (n - i) & 63 : 64;
    int l2 = l & 63;
    // signed shift (>>) とした場合空いた部分は元の符号 bit によって埋められるため,unsigned shift (>>>) を用いる必要がある.
    long v1 = a[k + p - 1] >>> (l1 - l2);
    long v2 = y[p - 1];
    sum += Long.bitCount(v1 ^ v2);
    ans = Math.min(ans, sum);
}
pw.println(ans);

AtCoder Grand Contest 043 D - Merge Triplets 定数倍改善

editorial では最後のパートを DP で解いているが,組み合わせ的に処理すると定数倍の軽い  O(N ^ 2) で解くことが出来る.

集合 \displaystyle \left\{i\;\middle|\; i=0\text{ または } i \gt 0 \text{ かつ }P _ {i} \gt \max _ {0\leq j\lt i} P _ j\right\} を昇順にソートしてできる列を  T とする.さらに, T の長さを  L C _ j = \#\{ 1 \leq i \lt L \mid T _ i - T _ {i - 1} = j \} とする.

結論から言えば,順列  P を作ることが出来るための必要十分条件は以下が成り立つことである.

  •  C _ j = 0\quad (j\geq 4)
  •  C _ 2 + C _ 3 \leq N

以下, X=C _ 1 Y=C _ 2 Z=C _ 3 とする.

 0\leq Y + Z\leq N を満たす  Y,  Z を全探索して  P を数え上げる.まず, X Y,  Z を用いて  X=3N-2Y-3Z と表せる.

 X 個の長さ  1 の列に含まれる要素の選び方は  \dfrac{1}{X!}\begin{pmatrix}3N\\ 1\end{pmatrix}\times\cdots\times\begin{pmatrix}3N-X+1\\ 1\end{pmatrix} 通り, Y 個の長さ  2 の列に含まれる要素の選び方は  \dfrac{1}{Y!}\begin{pmatrix}3N-X\\ 2\end{pmatrix}\times\cdots\times\begin{pmatrix}3N-X-2Y+2\\ 2\end{pmatrix} 通り, Z 個の長さ  3 の列に含まれる要素の選び方は  \dfrac{1}{Z!}\begin{pmatrix}3N-X-2Y\\ 3\end{pmatrix}\times\cdots\times\begin{pmatrix}3N-X-2Y-3Z+3\\ 3\end{pmatrix} 通りである.

順列  P において  X+Y+Z 個の列は最大値の小さい順に現れるので,列の並べ方は  1 通りに定まる.

各列はその最大値が先頭に来るように並べる必要があるので,長さ  j に対して  (j-1)! 通りの並べ方が存在する.

以上より,

 \displaystyle
\begin{align}
&(0!)^X\times\dfrac{1}{X!}\begin{pmatrix}3N\\ 1\end{pmatrix}\times\cdots\times\begin{pmatrix}3N-X+1\\ 1\end{pmatrix}\\
\times
&(1!)^Y\times\dfrac{1}{Y!}\begin{pmatrix}3N-X\\ 2\end{pmatrix}\times\cdots\times\begin{pmatrix}3N-X-2Y+2\\ 2\end{pmatrix}\\
\times
&(2!)^Z\times\dfrac{1}{Z!}\begin{pmatrix}3N-X-2Y\\ 3\end{pmatrix}\times\cdots\times\begin{pmatrix}3N-X-2Y-3Z+3\\ 3\end{pmatrix}\\
=&\frac{(3N)!}{X!\times Y!\times Z!\times 1^X\times 2^Y\times 3^Z}
\end{align}

 Y,  Z を固定した場合の寄与である.以上より,求める答えは次で表される.

 \displaystyle
\sum_{Y=0}^{N}\sum_{Z=0}^{N-Y}\frac{(3N)!}{(3N-2Y-3Z)!\times Y!\times Z!\times 2^Y\times 3^Z}

実装 (Python)

PyPy3 (7.3.0) で以下のコードを提出して 273 ms で AC が得られた.

N, M = map(int, input().split())
T = 3 * N

def mul_mod(*iterable):
    res = 1
    for v in iterable:
        res = res * v % M
    return res

facT = mul_mod(*range(1, T + 1))

fac_inv = [0] * T + [pow(facT, M - 2, M)]
for i in range(T, 0, -1):
    fac_inv[i - 1] = fac_inv[i] * i % M

pow2_inv = [1] + [0] * N
inv_2 = pow(2, M - 2, M)
for i in range(N):
    pow2_inv[i + 1] = pow2_inv[i] * inv_2 % M

pow3_inv = [1] + [0] * N
inv_3 = pow(3, M - 2, M)
for i in range(N):
    pow3_inv[i + 1] = pow3_inv[i] * inv_3 % M

print(sum(sum(mul_mod(facT, fac_inv[T - 2 * y - 3 * z], fac_inv[y], fac_inv[z], pow2_inv[y], pow3_inv[z]) for z in range(N + 1 - y)) for y in range(N + 1)) % M)

パナソニックプログラミングコンテスト(AtCoder Beginner Contest 195)

橙 perf 域から外れてしまった.

A - Health M Death

H % M == 0 ? "Yes" : "No"

B - Many Oranges

個数  k を決め打って,実現可能かどうかを考える. k 個のみかんの重さとしてあり得る最小値  L_k L _ k =kA で,最大値  R _ k R _ k = kB である.従って, L _ k \leq W \leq R _ k が実現可能であるための必要条件である.実はこれは十分条件にもなっている.

理由としては,すべてのみかんの重みを  \dfrac{W}{k} にすれば, A\leq \dfrac{W}{k} \leq B かつ  \displaystyle\sum _ {i=1} ^ k \dfrac{W}{k}=W が成立するためである.

なお,みかんの重みが整数に限られている場合でも,全てのみかんの重みを  \left\lfloor\dfrac{W}{k}\right\rfloor にしてから, W-k\cdot\left\lfloor\dfrac{W}{k}\right\rfloor 個のみかんの重みを  +1 してあげればよい. W=kB の場合は  +1 すると  B より大きくなってしまうが,このとき  W-k\cdot\left\lfloor\dfrac{W}{k}\right\rfloor=0 なので問題ない.

C - Comma

まず, d 桁の数に打つコンマの数は  \left\lfloor\dfrac{d - 1}{3}\right\rfloor である.

 N の桁数を  L とすると, d=1,\ldots,L-1 桁の任意の数は  N 以下であり,各  d に対して  10 ^ d - 10 ^ {d - 1} 個存在する.

また, L 桁でかつ  N 以下の数の個数は  N+1-10 ^ {L-1} である.

以上より,求める答えは以下のように表せる.

 \displaystyle
\sum_{d=1}^{L-1}\left\lfloor\dfrac{d - 1}{3}\right\rfloor\cdot\left(10^d-10^{d-1}\right)+\left\lfloor\dfrac{L - 1}{3}\right\rfloor\cdot\left(N+1-10^{L-1}\right)

D - Shipping Center

まず,クエリ毎に解法を考えてみる.

使える箱の個数を  K とする.価値の高い順に,箱のサイズが出来るだけ小さいものを選択して荷物を入れていくという貪欲法により解くことが出来る.

このような貪欲法を考えるのは,直感的には以下のような理由による.

  • 先に価値の低い荷物が箱を使ってしまった場合,後から価値の高い荷物を入れる箱がなくなってしまって損する可能性がある
  • 先に大きい箱を使ってしまった場合,後で大きい荷物が来た場合に入れられなくなって損する可能性がある

クエリ毎の計算量は,荷物のソートを事前に済ませておけば入れる箱を毎回線形探索しても  O(NM) である.従って,全体  O(N\log N + QNM) でこの問題を解くことが出来る.

E - Lucky 7 Battle

次のような DP を考える.

 \displaystyle
\mathrm{dp}[i][j]=\text{残り $i$ ターンで $T \equiv j\;(\mathrm{mod}\ 7)$ であるとき,高橋君は勝つことができるか}

ゲームのルールから, i=0 の場合に関して以下が成り立つ.

 \displaystyle
\mathrm{dp}[0][j]=\begin{cases}
\mathrm{True} & j=0\\
\mathrm{False} & j=1,\ldots,6
\end{cases}

ゲームを後ろから見る関係上,以降は  X および  S を反転しておくことにする

残り  i ターンの時点で  d が加えられた場合, j の値は  j+d\cdot 10 ^ {i-1} \bmod 7 へと変化する.従って,DP の遷移は次のように書くことが出来る.

 \displaystyle
\mathrm{dp}[i][j]=\begin{cases}
\mathrm{dp}[i-1][j]\lor\mathrm{dp}[i-1][j+S_i\cdot 10^{i-1} \bmod 7]& \text{if $X_i=\text{'T'}$} \\
\mathrm{dp}[i-1][j]\land\mathrm{dp}[i-1][j+S_i\cdot 10^{i-1} \bmod 7] &\text{if $X_i=\text{'A'}$}
\end{cases}

答えは, \mathrm{dp}[N][0] \mathrm{True} なら高橋君,そうでなければ青木君である.

F - Coprime Present

未証明.追加可能な数の集合を状態としてメモ化再帰をすると間に合って解ける.

from math import gcd

A, B = map(int, input().split())
W = B - A + 1
coprime = [[i < j and gcd(A + i, A + j) == 1 for j in range(W)] for i in range(W)]
memo = {}

def dfs(s):
    if s in memo:
        return memo[s]
    res = 1
    bits = [i for i in range(W) if s >> i & 1]
    for i in bits:
        t = 0
        for j in bits:
            t |= coprime[i][j] << j
        res += dfs(t)
    memo[s] = res
    return res

print(dfs((1 << W) - 1))

functools.lru_cache を使えばメモ用の dict を持たなくてもよい.

from functools import lru_cache
from math import gcd

A, B = map(int, input().split())
W = B - A + 1
coprime = [[i < j and gcd(A + i, A + j) == 1 for j in range(W)] for i in range(W)]

@lru_cache(maxsize=1 << 32)
def dfs(s):
    res = 1
    bits = [i for i in range(W) if s >> i & 1]
    for i in bits:
        t = 0
        for j in bits:
            t |= coprime[i][j] << j
        res += dfs(t)
    return res

print(dfs((1 << W) - 1))

AtCoder Beginner Contest 194

A から D までは良かったのに,その後グダりまくってしまった.

A - I Scream

読解に少し時間が掛かった.

if (a + b >= 15 && b >= 8) {
    pw.println(1);
} else if (a + b >= 10 && b >= 3) {
    pw.println(2);
} else if (a + b >= 3) {
    pw.println(3);
} else {
    pw.println(4);
}

B - Job Assignment

 \displaystyle \min\{\min _ {1\leq i\leq N} \{A _ i + B _ i\}, \min _ {\overset{\scriptstyle 1\leq i, j \leq N,}{i\neq j}} \{\max\{A _ i, B _ j\}\} \} が答え.式にするとややこしいけど,実装はシンプル.

 N が小さいので,すべての  i,  j の組を全探索しても間に合う.

この問題では求められていないが, \displaystyle\min _ {\overset{\scriptstyle 1\leq i, j \leq N,}{i\neq j}} \{\max\{A _ i, B _ j\}\}=\min _ {1\leq i \leq N} \{\max\{A _ i, \min _ {j\neq i} B _ j\}\} なので,左右からの累積  \min を持って  O(N) で求めることもできる.

long min = Long.MAX_VALUE;
for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        long v;
        if (i == j) {
            v = a[i] + b[i];
        } else {
            v = Math.max(a[i], b[j]);
        }
        min = Math.min(min, v);
    }
}
pw.println(min);

C - Squared Error

計算パートが重め.

求める答えを  S とおく.

 \displaystyle
\begin{aligned}
S
&=\sum_{1\leq i\lt j\leq N}(A_i-A_j)^2\\
&=(N-1)\sum_{i=1}^N A_i^2-2\sum_{1\leq i\lt j\leq N}A_iA_j\\
&=(N-1)\sum_{i=1}^N A_i^2-\left(\sum_{i=1}^N A_i\right)^2+\sum_{i=1}^N A_i^2\\
&=N\sum_{i=1}^N A_i^2-\left(\sum_{i=1}^N A_i\right)^2
\end{aligned}

より,これは  O(N) で計算できる.

D - Journey

高橋君のいる連結成分のサイズを  K とする.このとき,次の試行が成功する確率は  \dfrac{N-K}{N} なので,成功するまでの試行回数の期待値は  \dfrac{N}{N-K}.従って,これを  K=1,\ldots,N-1 について足し合わせれば答えが求まる.つまり,以下を計算すれば良い.

 \displaystyle
\sum_{K=1}^{N-1}\frac{N}{N-K}=N\sum_{i=1}^{N-1}\frac{1}{i}


double p = 0;
for (int i = 1; i < n; i++) {
    p += 1. / i;
}
pw.println(n * p);

E - Mex Min

初めはセグ木で殴ろうとして書いてたけど,書きあげてから制約を見て冷静になった.

 \displaystyle
\mathrm{last}_{i, j}:=\text{$A_1,\ldots,A_i$ において最後に $j$ が現れた添え字}

を管理する.ただし,

 \displaystyle
\mathrm{last} _ {0, j}=-1,\quad j=0,1,\ldots

とする. i 毎に更新箇所は高々 1 箇所しかないので,in-place に更新すればこれは  O(N) で求まる.

 \mathrm{last} _ {i, j} の更新時にその差分が  M 以上となる  i が存在するか,または  \mathrm{last} _ {N, j}\lt N-M となるような  j の最小値が答えとなる.

F - Digits Paradise in Hexadecimal

 \displaystyle
\begin{aligned}
\mathrm{dp}_{i, j}:=
&\text{上から $i$ 桁を見終わった時点で $N$ 未満であることが確定していて,}\\
&\text{かつ使った種類数が $j$ であるような場合の数}
\end{aligned}

という定義で桁 DP をする.

まず, i 桁目時点で既に  N 未満であることが確定しているような場合の遷移について考える.このとき, i+1 桁目はどの数であっても次もまた  N 未満である.種類数が変わらないのが  j 通り,種類数が  1 だけ増えるのが  16-j 通りなので,以下のような遷移が書ける.

dp[i + 1][j] += dp[i][j] * j
dp[i + 1][j + 1] += dp[i][j] * (16 - j)

続いて, i 桁目時点で  N と等しい場合の遷移について考える. N の上から  i 桁に含まれる数の集合を  S_i とする.また, N の上から  i 桁目の数を  d_i とする.

このとき, i+1 桁目を加えて  N 未満となるのは, 0,\ldots,d _ {i + 1} -1 を加えた場合である.このうち, S _ i に含まれる要素を追加した場合は種類数は変わらず,含まれない要素を追加した場合は種類数が  1 だけ増える.従って,擬似的には以下のような遷移となる.

for k = 0, ..., d[i+1] - 1:
    if k in S[i]:
        dp[i + 1][cardinality(S[i])] += 1
    else:
        dp[i + 1][cardinality(S[i]) + 1] += 1

基本的な考え方はこれでよいが,先頭の 0 の扱いについては注意する必要がある.

まず,直前に示した擬似コードにおいて, i=0 の場合は  j のループ範囲を  1,\ldots,d _ {i+1} - 1 とする必要がある (先頭の 0 を種類数に勘定しないようにするため).しかし,こうすると今度は桁数が  N より小さい数を丸ごと数え漏らしてしまうことになるので,この分を補う必要がある.

 N より桁数が小さい任意の数は  N 未満であるので,この場合の寄与は DP とは別に組み合わせ的に数えて答えに足し合わせても良いし,DP に組み込んでも良い.DP に組み込む方が簡単で, \mathrm{dp} _ {i, 1}\;(i\geq 2) に先頭としてあり得る (0 を除いた)  15 通りを足してあげればよい.