AtCoder Grand Contest 051 (Good Bye rng_58 Day 2) A,B,C,D問題メモ
A - Dodecagon
問題
解法
こういうのは制約のある部分=外周から埋めていくのがセオリー。
フリーハンドで図を書いて試すのが難しいが、がんばる。CAD使えたら強そう。
直線部分は、正方形を置いたら隣に正三角形は並べられず、逆も然りなので、片方だけをずっと敷いていくしかない。
逆に角は、正十二角形の1つの内角が150°なので、正方形と正三角形を置いたらちょうど埋められる。
【下辺が直線になるように敷き詰めたい】
↓この30度の隙間は埋められない
×: □△
○: □□□ or △▽△▽△ しかない
【角を埋めたい】
□▷
↑ここが150°となり、ちょうど正十二角形と合う
どこか1個の角に正方形と正三角形を置き、埋められない角が出来ないように外周を連鎖的に決めていくと、
辺ごとに正三角形と正方形を交互に敷き詰めると綺麗に埋まり、逆にこれしかないことがわかる。
その時、内側にはまた十二角形が出来るのだが、正三角形を敷き詰めた辺のみ $d-1$、正方形を敷き詰めた方は $d$ のままという形になっている。
内側の十二角形は同じ理屈で外周を交互に敷き詰めることで埋められて、さらに内側には同じように、正三角形を敷き詰めた辺のみ長さの1減った十二角形ができる。
$(d,d)→(d-1,d)→(d-2,d)→(d-2,d-1)→...→(0,0)$ というように、どちらかを1減らしながら $(0,0)$ になるまでの経路数と考えられ、二項係数 ${}_{2d}C_{d}$ と一致する。
回転させての重複を除いて、この半分が答え。
Python3 小さいサンプルでの実験コード
import sys
from functools import lru_cache
sys.setrecursionlimit(10 ** 6 + 10)
@lru_cache(maxsize=None)
def solve(a, b):
if a == 0:
return 1
ret = solve(a - 1, b)
if a == b:
ret += solve(b - 1, a)
else:
ret += solve(a, b - 1)
return ret
for d in range(1, 11):
print(solve(d, d))
Python3 解答コード
def prepare(n, MOD):
f = 1
factorials = [1]
for m in range(1, n + 1):
f = f * m % MOD
factorials.append(f)
f = pow(f, MOD - 2, MOD)
finvs = [1] * (n + 1)
finvs[n] = f
for m in range(n, 1, -1):
f = f * m % MOD
finvs[m - 1] = f
return factorials, finvs
d = int(input())
MOD = 998244353
facts, finvs = prepare(2 * d, MOD)
print(facts[2 * d] * finvs[d] * finvs[d] * finvs[2] % MOD)
寄木細工の職人さんとか、経験的に知ってたりするのかな。
B - Bowling
問題
広い空間に、ボウリングのピンを置く
$A,B,C,D$ の4人が、それぞれ →,↗,↑,↖ 方向からこれを見る
重なっている後ろのピンは見えない(厳密には問題ページ参照)
それぞれから見える本数を $a,b,c,d$ とすると、$d \ge 10 \max(a,b,c)$ となるような配置を1つ構築せよ
置ける本数は $10^5$ 本まで
解法
ボウリングで1-5や2-8など縦に2つ並ぶように残った形は真正面から狙いたくなるが、その場合本当に真正面から当てないと前のピンに弾かれて後ろが残りやすい。
なるべくナナメから狙った方が許容範囲が広い。
$A$ と $D$ だけ考えると、少なくとも10本は必要。
A→ ○○○○○○○○○○
↖D
$C$ を考慮に加えると、$A$ から重なって見えている2本が $C$ からも重なって見えることはないので、10行に増やして対処する。
その際、$D$ からは増やした行を含めて全て重ならないように、行の間を十分空ける。
○○○○○○○○○○
↕10行
○○○○○○○○○○
↕10行
A→ ...
↕10行
○○○○○○○○○○
↑C ↖D
更にこの大きな塊を10回 ↗ 方向に繰り返すと、それが答え。
使うピンの本数は1000本。
Python3
本数最小化チャレンジ
どうも、⊿ の形が強いらしい。$A,B,C$ からは2本、$D$ からは3本見える。
これをフラクタルのように(重ならないように多少ずらしつつ)繰り返すと、
⊿ A,B,C: 4本
⊿⊿ D : 9本
⊿
⊿⊿ A,B,C: 8本
⊿ ⊿ D : 27本
⊿⊿⊿⊿
と、$A,B,C$ は2倍、$D$ は3倍になっていく。よって6段階目に64 vs 729 となり、比が10倍を超える。
C - Flipper
問題
無限に広がるグリッドがある
$N$ マス $(x_1,y_1),(x_2,y_2),...,(x_N,y_N)$ が黒く、他が白く塗られている
以下の操作を好きなだけ行える
操作
操作後の黒マスの個数の最小値を求めよ
$1 \le N \le 10^5$
$1 \le x_i,y_i \le 10^9$
解法
操作を上手く言い換えたいが、3マスを同時に反転というのがなかなか見たことない操作なので、悩ましい。
不変量
列ごとのXOR
黒を1、白を0とする。
ドミノや畳のように2×1のマスを同時に反転させるとき、その列のXORが不変量として挙げられる。
# #
[.] [#]
[#] → [.]
. .
XOR = 0 0
この問題でも同様に、縦方向に関しては同時に反転させる個数は2マスなので、列ごとのXORは何回操作しても変わらない。
初期状態でXOR=1の列(黒マスが奇数個ある列)があったら、その列はどうやっても0個にはできない。
行ごとのXOR
少し観察する。なるべく上の、同じ行なら左の黒マスを消すようにしてみると、
..#..#.. → ...###.. → ........
........ ..###... ..#..#..
..#.##..#.. → ...#.#..#.. → ....#...#.. → .....##.#.. → .......##..
........... ..###...... ..#..#..... ..#.#.#.... ..#.##.#...
1つの行だけのことを考え、他の行に及ぼす影響はひとまず無視すると、
ことがわかる。
よって、行ごと・列の $\mod{3}$ ごとにXORをとると、各行の $\mod{3}$ が $0,1,2$ となる列で、黒マスを消せるか、残ってしまうかがわかる。
j%3 01201201201 j%3 0 1 2 列番号を3で割ったあまりが1と2の列のどこかには、
..#.##..#.. → xor (0,1,1) 少なくとも1個ずつ、黒マスが残る
(1,0,0) 操作して反転させ、あまりが0の列のどこかで
黒マスが残る、という状態にすることもできる
残り方の例
01201201201 01201201201
..#....#... ...#.......
3列中2列で残ってしまうなら、反転させて1列でしか残らない方が基本的にはいい。
これは、$(1,1,1)$ となってしまったときも同様で、反転させて $(0,0,0)$ にした方がよい。
ただし、どこか1行だけを反転させることはできず、他の行とニコイチになる。全ての行での延べ反転回数は偶数回でないといけない。
012 012
.## →×→ #.. 個別に反転させることはできない
#.. #..
.## #.. 2行セットなら反転できる。
#.. →○→ #.. 縦に連続してなくてもよい。
##. ..# (1行ずつずらしながら操作すると、波及させていけるので)
この条件の元で、「列のXORから残ってしまう黒マス」と、「行のXORから残ってしまう黒マス」をなるべく多くマッチングさせる問題となる。
どうせ残すなら、1個の黒マスを「行でも列でも残ってしまう位置」に配置した方がよい。
黒マスを減らす
行ごとのXORは、反転を適宜おこなうと $\mod{3}$ で「どの行もXOR=0」「0の列だけがXOR=1」「1の列だけがXOR=1」「2の列だけがXOR=1」に分類される。
分類毎に行数をカウントしておく。
列ごとのXORも、列番号の $\mod{3}$ ごとにカウントしておく。
0 1 2
行 3 3 8 ←行ごとのXORで「mod3が0/1/2の列だけがXOR=1」となる行の数
列 5 7 2 ←列ごとのXORで、mod3が0/1/2ごとに、XOR=1となった列の数
貪欲にマッチングさせる。どちらか一方がいくらか余る。
0 1 2
行 0 0 6
列 2 4 0
ここで、行に関しては $(1,0,0)→(0,1,1)$ に反転させなおすことが可能である。
行単独で見れば黒マスの個数は増えてしまうが、もうその列で打ち消せる黒マスが無く、かつ他の列には黒マスが余っている場合のみ、有効な手立てとなる。
0 1 2 0 1 2 0 1 2
行 0 0 6 → 行 2 2 4 → 行 0 0 4
列 2 4 0 列 2 4 0 列 0 2 0
また、ここまでで行を反転させた回数が奇数回の場合は、どれでもいいのでどれか1個元に戻さないといけない。
0 1 2 0 1 2 0 1 2
行 0 0 4 → 行 1 1 3 → 行 1 0 3
列 0 2 0 列 0 2 0 列 0 1 0
このようにして、マッチングで相殺した黒マス $13$ 個に、マッチングできずに余っている $1+1+3=5$ 個を加えた $18$ 個が、この場合の答えとなる。
注意点として、反転が奇数回だった場合の戻し方で、$(1,0,0), (0,1,1)$ など0と1が混ざった行が1個でもあればそこを戻せばよいが、
もし全て $(0,0,0),(1,1,1)$ の行だった場合、元に戻せるのは $(0,0,0)→(1,1,1)$ しかないので戻し方が異なる。
しかしその場合、行のXORは全て0、列のXORは0以上の同じ値になっているはずなので、単に列のXORの合計が答えとなり、結果的に無視しても問題ない。
Python3
from collections import defaultdict
n = int(input())
x_xor = defaultdict(lambda: [0, 0, 0])
y_xor = defaultdict(int)
for _ in range(n):
x, y = map(int, input().split())
x_xor[x][y % 3] ^= 1
y_xor[y] ^= 1
y_mod = [0, 0, 0]
for y, xor in y_xor.items():
y_mod[y % 3] += xor
x_mod = [0, 0, 0]
reverse_rows = 0
exists_011_rows = False
for x, xor in x_xor.items():
bit = xor[0] + xor[1] * 2 + xor[2] * 4
if bit == 1:
x_mod[0] += 1
elif bit == 2:
x_mod[1] += 1
elif bit == 3:
x_mod[2] += 1
reverse_rows ^= 1
exists_011_rows = True
elif bit == 4:
x_mod[2] += 1
elif bit == 5:
x_mod[1] += 1
reverse_rows ^= 1
exists_011_rows = True
elif bit == 6:
x_mod[0] += 1
reverse_rows ^= 1
exists_011_rows = True
elif bit == 7:
reverse_rows ^= 1
diff = [ym - xm for xm, ym in zip(x_mod, y_mod)]
for i in range(3):
j = (i + 1) % 3
k = (i + 2) % 3
if diff[i] < 0:
adjust = min(diff[j], diff[k])
if adjust > 0:
if reverse_rows ^ (adjust % 2):
adjust -= 1
reverse_rows = 0
diff[i] += adjust
diff[j] -= adjust
diff[k] -= adjust
break
ans = sum(y_mod)
if reverse_rows:
if exists_011_rows:
if diff[0] <= 0 and diff[1] <= 0 and diff[2] <= 0:
ans += 1
else:
diff[0] -= 1
diff[1] -= 1
diff[2] -= 1
for i in range(3):
if diff[i] < 0:
ans += -diff[i]
print(ans)
D - C4
問題
d
S-----V
a | | c
T-----U
b
頂点 $S,T,U,V$ を上図のようにつないだ無向グラフがある
$S$ からスタートして $S$ で終わるようなウォークのうち、$ST,TU,UV,VS$ の辺をそれぞれ $a,b,c,d$ 回通るものの個数を $\mod{998244353}$ で求めよ
$1 \le a,b,c,d \le 5 \times 10^5$
解法
数え上げの立式・式変形+たたみ込み演算。
不可能な場合を先に除外しておく。
$S$ を出発して $S$ に戻ってくるにあたって、ぐるっと1周すると各辺を1回ずつ消費し、そうで無い場合は通った辺について往復で2回ずつ消費する。
そのため、ウォークが構成可能であるためには「$a,b,c,d$ の偶奇が全て一致している」必要がある。
偶奇が異なれば0通りを出力して終了。
偶奇が同じなら、規定の回数になるまで各辺で往復しまくれば、個数はともかくウォークが構成可能なことは示せる。
大まかな数え上げの方針
2回の移動を1セットとして考えると、移動終了後の位置は $S$ または $U$ に限られてパターンが考えやすくなる。
① $S→T→U$: $a,b$ を1回ずつ消費
② $U→T→S$: $a,b$ を1回ずつ消費
③ $S→V→U$: $c,d$ を1回ずつ消費
④ $U→V→S$: $c,d$ を1回ずつ消費
⑤ $S→T→S$: $a$ を2回消費
⑥ $U→T→U$: $b$ を2回消費
⑦ $U→V→U$: $c$ を2回消費
⑧ $S→V→S$: $d$ を2回消費
①~④の使いどころを先に決めた後、⑤~⑧を適当な位置に挟み込むことでウォークを構成することを考える。
①と②を計 $i$ 回、③と④を計 $j$ 回行ったと決め打つと、$i+j$ 回の $S→U→S→...$ の切り替わりが発生する。
i=2, j=4 としたときの一例
S----U----S----U----S----U----S
① ④ ③ ② ③ ④ (ここで①~④の並べ方によるパターンが発生する)
$a,b$ はそれぞれ $i$ 回、$c,d$ はそれぞれ $j$ 回消費されている。
よって残りを消費しきるためには、⑤⑥⑦⑧ をそれぞれ $\dfrac{a-i}{2},\dfrac{b-i}{2},\dfrac{c-j}{2},\dfrac{d-j}{2}$ 回行う必要がある。
($i,j$ は、$a~d$ が全て偶数回残るように決める必要がある)
S------U--------S----------U------S--------U------S----|
⑤① ⑥⑦④ ⑧⑤⑧③ ⑥② ⑧⑧③ ⑦④ ⑧
~~ ~~~~ ~~~~~~ ~~ ~~~~ ~~ ~~
これでウォークが完成する。
$i,j$ の制約は以下の通り。
具体的な数え上げ
①~④の並べ方
①③と②④は交互に現れると決まっているので、$i+j$ 回のうちどれを①②にしてどれを③④にするか決めれば、並べ方は一意に決まる。
i=2 j=2 ⑫=①または②、㉞=③または④を意味するとする
⑫⑫㉞㉞ → ①②③④
⑫㉞㉞⑫ → ①④③②
よって、${}_{i+j}C_{i} = \dfrac{(i+j)!}{i!j!}$ 通り。
⑤⑧の並べ方
先ほどの図から、$S$ を出発点としたものだけを抜き出すと
S------U-S----------U-S--------U-S----|
⑤① ⑧⑤⑧③ ⑧⑧③ ⑧
これは、以下のボールを一列に並べる場合の数と一致する。
赤いボール: 既定の①③の位置を示す。$\dfrac{i+j}{2}$ 個
青いボール: ⑤の挟み込み位置を示す。$\dfrac{a-i}{2}$ 個
緑のボール: ⑧の挟み込み位置を示す。$\dfrac{d-j}{2}$ 個
つまり、$\dfrac{\frac{a+d}{2}!}{\frac{i+j}{2}! \frac{a-i}{2}! \frac{d-j}{2}!}$ となる。
⑥⑦の並べ方
同様に $U$ を出発点としたものだけを抜き出すと
S-U--------S-U------S-U------S-|
⑥⑦④ ⑥② ⑦④
以下のボールを一列に並べる場合の数と一致する。
赤いボール: 既定の②④の位置を示す。ただし末尾は必ず②④固定なので $\dfrac{i+j}{2}-1$ 個
青いボール: ⑥の挟み込み位置を示す。$\dfrac{b-i}{2}$ 個
緑のボール: ⑦の挟み込み位置を示す。$\dfrac{c-j}{2}$ 個
つまり、$\dfrac{(\frac{b+c}{2}-1)!}{(\frac{i+j}{2}-1)! \frac{b-i}{2}! \frac{c-j}{2}!}$ となる。
まとめ
$i,j$ ごとに以上を掛け合わせて合計したものが答えとなる。うげぇとなりそうな式である。
$\displaystyle \sum_{i,j} \left ( \frac{(i+j)!}{i!j!} \times \frac{\frac{a+d}{2}!}{\frac{i+j}{2}! \frac{a-i}{2}! \frac{d-j}{2}!} \times \frac{(\frac{b+c}{2}-1)!}{(\frac{i+j}{2}-1)! \frac{b-i}{2}! \frac{c-j}{2}!} \right )$
たたみ込みへの変形
上記を全ての $i,j$ について求めて合計すればよいのだが、$i,j$ はそれぞれ $5 \times 10^5$ までの値を取り得るので、愚直には無理。
ここで、上記の式を「定数項」「$i+j$ に依存する項」「$i$ に依存する項」「$j$ に依存する項」に分けてやる。
$\displaystyle \frac{a+d}{2}! (\frac{b+c}{2}-1)! \sum_{i,j} \left ( \frac{(i+j)!}{\frac{i+j}{2}! (\frac{i+j}{2}-1)!} \times \frac{1}{i! \frac{a-i}{2}! \frac{b-i}{2}!} \times \frac{1}{j! \frac{c-j}{2}! \frac{d-j}{2}!} \right )$
すると、「$i$ に依存する項」と「$j$ に依存する項」は、あらかじめ独立に $i=0,1,2,...$ と $j=0,1,2,...$ の場合を計算しておき、
FFTによるたたみ込みを使えば $O(N \log{N})$ で「$i+j$ に依存する項」に変換できる。
そして、$i+j$ ごとに「元々の $i+j$ に依存する項」×「たたみ込みによる $i+j$ の項」を合計してやれば、Σの中身が高速に計算可能になる。
Python3
import os
import sys
import numpy as np
def solve(inp):
def bit_length(n):
x = 0
while 1 << x < n:
x += 1
return x
def bit_scan_forward(n):
x = 0
while n & 1 == 0:
n >>= 1
x += 1
return x
def pow_mod(x, n, m):
r = 1
y = x % m
while n:
if n & 1:
r = (r * y) % m
y = y * y % m
n >>= 1
return r
def get_primitive_root(m):
if m == 2: return 1
if m == 167772161: return 3
if m == 469762049: return 3
if m == 754974721: return 11
if m == 998244353: return 3
divs = [2]
x = (m - 1) // 2
while x & 1 == 0:
x >>= 1
i = 3
while i * i <= x:
if x % i == 0:
divs.append(i)
x //= i
while x % i == 0:
x //= i
i += 2
if x > 1:
divs.append(x)
g = 2
while True:
ok = True
for d in divs:
if pow_mod(g, (m - 1) // d, m) == 1:
ok = False
break
if ok:
return g
def butterfly_prepare(mod, primitive_root):
sum_e = np.zeros(30, np.int64)
sum_ie = np.zeros(30, np.int64)
es = np.zeros(30, np.int64)
ies = np.zeros(30, np.int64)
cnt2 = bit_scan_forward(mod - 1)
e = pow_mod(primitive_root, (mod - 1) >> cnt2, mod)
ie = pow_mod(e, mod - 2, mod)
for i in range(cnt2, 1, -1):
es[i - 2] = e
ies[i - 2] = ie
e = e * e % mod
ie = ie * ie % mod
now_e = 1
now_ie = 1
for i in range(cnt2 - 1):
sum_e[i] = es[i] * now_e % mod
sum_ie[i] = ies[i] * now_ie % mod
now_e = now_e * ies[i] % mod
now_ie = now_ie * es[i] % mod
return sum_e, sum_ie
def butterfly(aaa, mod, sum_e):
n = aaa.size
h = bit_length(n)
for ph in range(1, h + 1):
w = 1 << (ph - 1)
p = 1 << (h - ph)
now = 1
for s in range(w):
offset = s << (h - ph + 1)
for i in range(p):
l = aaa[i + offset]
r = aaa[i + offset + p] * now % mod
aaa[i + offset] = (l + r) % mod
aaa[i + offset + p] = (l - r) % mod
now = now * sum_e[bit_scan_forward(~s)] % mod
def butterfly_inv(aaa, mod, sum_ie):
n = aaa.size
h = bit_length(n)
for ph in range(h, 0, -1):
w = 1 << (ph - 1)
p = 1 << (h - ph)
inow = 1
for s in range(w):
offset = s << (h - ph + 1)
for i in range(p):
l = aaa[i + offset]
r = aaa[i + offset + p]
aaa[i + offset] = (l + r) % mod
aaa[i + offset + p] = ((l - r) * inow) % mod
inow = inow * sum_ie[bit_scan_forward(~s)] % mod
def convolution(aaa, bbb, MOD):
n = aaa.size
m = bbb.size
k = n + m - 1
z = 1 << bit_length(k)
raaa = np.zeros(z, np.int64)
rbbb = np.zeros(z, np.int64)
raaa[:n] = aaa
rbbb[:m] = bbb
primitive_root = get_primitive_root(MOD)
sum_e, sum_ie = butterfly_prepare(MOD, primitive_root)
butterfly(raaa, MOD, sum_e)
butterfly(rbbb, MOD, sum_e)
for i in range(z):
raaa[i] = raaa[i] * rbbb[i] % MOD
butterfly_inv(raaa, MOD, sum_ie)
iz = pow_mod(z, MOD - 2, MOD)
for i in range(k):
raaa[i] = raaa[i] * iz % MOD
return raaa[:k]
def prepare_factorials(n, MOD):
factorials = np.ones(n + 1, np.int64)
for m in range(1, n + 1):
factorials[m] = factorials[m - 1] * m % MOD
inversions = np.ones(n + 1, np.int64)
inversions[n] = pow_mod(factorials[n], MOD - 2, MOD)
for m in range(n, 1, -1):
inversions[m - 1] = inversions[m] * m % MOD
return factorials, inversions
MOD = 998244353
a, b, c, d = inp
if a % 2 == b % 2 == c % 2 == d % 2:
is_odd = a % 2
else:
return 0
facts, finvs = prepare_factorials(1001001, MOD)
if b == 0 and c == 0:
l = (a + d) // 2
return facts[l] * finvs[a // 2] // MOD * finvs[d // 2] % MOD
iii = np.zeros(min(a, b) // 2 + 1, np.int64)
jjj = np.zeros(min(c, d) // 2 + 1, np.int64)
for k in range(min(a, b) // 2 + 1):
i = is_odd + 2 * k
iii[k] = finvs[i] * finvs[(a - i) // 2] % MOD * finvs[(b - i) // 2] % MOD
for k in range(min(c, d) // 2 + 1):
j = is_odd + 2 * k
jjj[k] = finvs[j] * finvs[(c - j) // 2] % MOD * finvs[(d - j) // 2] % MOD
conv = convolution(iii, jjj, MOD)
ans = 0
for k in range(is_odd ^ 1, len(conv)):
ij = 2 * (k + is_odd)
tmp = facts[ij] * finvs[ij // 2] % MOD * finvs[ij // 2 - 1] % MOD
tmp = tmp * conv[k] % MOD
ans = (ans + tmp) % MOD
ans = ans * facts[(a + d) // 2] % MOD * facts[(b + c) // 2 - 1] % MOD
return ans
if sys.argv[-1] == 'ONLINE_JUDGE':
from numba.pycc import CC
cc = CC('my_module')
cc.export('solve', '(i8[:],)')(solve)
cc.compile()
exit()
if os.name == 'posix':
# noinspection PyUnresolvedReferences
from my_module import solve
else:
from numba import njit
solve = njit('(i8[:],)', cache=True)(solve)
print('compiled', file=sys.stderr)
inp = np.fromstring(sys.stdin.read(), dtype=np.int64, sep=' ')
ans = solve(inp)
print(ans)