−目次
AtCoder Grand Contest 043 A~D問題メモ
A - Range Flip Find Route
問題
- H×W マスの盤面、各マスは白または黒(配置は与えられる)
- 以下の操作を何回か繰り返して、以下の条件を満たすようにする
- 【操作】
- 好きな位置・大きさの矩形範囲を選び、その中の全てのマスの白と黒を反転させる
- 【条件】
- 左上から右下まで、常に右か下に移動するとき、白マスだけを通ってたどり着ける
- 必要な操作の最小回数を求めよ
解法
白黒の反転は、「境界を無くす」操作。
1次元配列だったら、SからGまで白黒の境界が4つあれば、そこを反転させる区間の端とすることで、両端で境界を2個ずつ無くせる。
S□■■■□□■■□□G |------------| |--|
2次元でも一緒で、右または下に進むときの白→黒、黒→白の境界を、1つの操作で最大2個、消滅させることができる。
矩形なので境界自体はもっと消滅させられるのだが、最終的なルートで通過しない境界をいじっても意味が無い。 →と↓にしか移動できないので、矩形範囲に入る・出るの2回のタイミングで通過する境界のみ効果がある。
よって、最短経路探索で、色の違うマスに移動するときコスト1、同じならコスト0としてSからGまでのコストを求め、それを2で割ればよい。(切り上げ)
S,Gが初期配置で黒の場合はそれぞれ+1する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
from collections import deque def solve(h, w, field): s = int (field[ 0 ][ 0 ] = = '#' ) q = deque([(s, s, 0 , 0 )]) visited = [[[ False ] * 2 for _ in range (w)] for _ in range (h)] while q: cost, state, i, j = q.popleft() if visited[i][j][state]: continue visited[i][j][state] = True if i = = h - 1 and j = = w - 1 : return cost for ni, nj in [(i, j + 1 ), (i + 1 , j)]: if ni = = h or nj = = w: continue nc = int (field[ni][nj] = = '#' ) if state = = 0 and nc = = 1 : q.append((cost + 1 , nc, ni, nj)) else : q.appendleft((cost, nc, ni, nj)) h, w = list ( map ( int , input ().split())) field = [ input () for _ in range (h)] print (solve(h, w, field)) |
B - 123 Triangle
問題
- 1,2,3のみからなる長さ N の数列が与えられる
- 隣り合う要素の差の絶対値を取って、次の数列とする(長さは1短くなる)
- つまり、最初の数列を X1={x1,1,x1,2,...,x1,N} として、以下のようになる
- x2,1=|x1,1−x1,2|
- x2,2=|x1,2−x1,3|
- …
- N−1 回繰り返し、長さが1になったとき、その値(xN,1)を求めよ
- 2≤N≤106
解法
何はともあれ実験が簡単にできるので、してみる。
まず、X2 以降は全て0,1,2となる。この3数から作られる差は2が最大、0が最小であって、それ以上・以下になることはない。
X1 も、1ずつ引いて0,1,2にしてしまっても問題ないのでそうする。
0 0 0 0 0 1 0 0 0 0 0 2 0 0 0 0 1 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 2 2 0 0 0 1 1 0 0 0 0 0 0 2 2 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 2 0 2 0 0 1 0 1 0 0 0 0 0 2 0 2 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 2 2 2 2 0 1 1 1 1 0 0 0 0 2 2 2 2 0 0 0 0 0 0 0 0 1 0 0 0 1 0 2 0 0 0 2 1 0 0 0 1 0 0 0 2 0 0 0 2 0 0 0 0 0 0 1 1 0 0 1 1 2 2 0 0 2 1 1 0 0 1 1 0 0 2 2 0 0 2 2 0 0 0 0 0 0 1 0 1 0 1 0 2 0 2 1 0 1 0 1 0 1 0 2 0 2 0 2 0 2 0 0 0 0 1 1 1 1 1 1 2 2 2 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 2 0 0 0 0 0 0 1 1 0 1 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 2 2 0 0 0 0 1 0 1 1 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 2 0 2 0 0 1 1 1 0 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0 2 2 2 0 1 0 0 1 1 0 0 1 0 0 0 1 0 0 0 1 0 0 0 2 0 0 1 1 0 1 0 1 0 1 1 0 0 1 1 0 0 1 1 0 0 2 2 0 0 1 1 1 1 1 1 0 1 0 1 0 1 0 1 0 1 0 2 0 2 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 1 1 0 1 0 0 1 0 1 0 0 0 0 0 0 0 0 1 0 1 1 0 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 0 0 1 1 0 0 0 0 0 1 1 0 1 1 0 1 0 1 0 0 0 0 1 0 1 0 1 1 1 1 1 0 0 0 1 1 1 1 0 0 0 0 1 0 0 1 0 0 1 0 0 0 1 1 0 1 1 0 1 0 0 1 0 1 1 0 1 1 0 1 1 1 0 1 1 1 1 0 0 1 1 0 0 1 0 1 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
0多めで実験してみると、1は三角形状に波及していき、シェルピンスキーの三角形が相互に作用しているような形状となる。
2も、隣が0なら同様に波及していくが、1と衝突すると消滅してしまう。これは1から見たとき、0も2も、1との差分は同じ1であることから、1の方が優位性が高いことが分かる。
またよく見ると、1の三角形同士が干渉した後も、一方の三角形で広範囲に0が続く箇所は、もう一方の三角形がそのまま描画されている。
ここから、1同士が干渉する箇所は、XORで考えればよいかも、と思いつく。
2は、1が含まれていればどこかで1と衝突して0と同じように扱われ最終結果に影響しない。よって0で置き換えるとする。
0,1だけに絞って考えると、差の絶対値の計算は、XORと同じ結果となる。
XOR |A-B| B\A| 0 1 B\A| 0 1 ---+------- ---+------- 0 | 0 1 0 | 0 1 1 | 1 0 1 | 1 0
なので、Xi から Xi+1 を作成するのには差の絶対値の代わりにXORで考えてもよい、とわかる。
それならば、1つずつに分解して考えられる。
初期状態で1である要素が、その位置から波及していって最下段で0になるか1になるかは、段数と位置から O(1) で計算できる。 その結果のXORを取ればよい。
ただし、初期状態に1が含まれない場合、2がそのまま影響する。しかしこれは、波及する要素が2になっただけで、1と同じ理論が使える。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
def test(a): print ( * a) for i in range ( len (a) - 1 ): a = [ abs (a1 - a0) for a0, a1 in zip (a, a[ 1 :])] print ( ' ' * i, * a) # print(*a) # test([2, 3, 1, 1, 3, 1, 2, 3, 1, 2]) # test([2, 3, 1, 1, 3, 2, 2, 3, 1, 2]) # test([2, 3, 1, 1, 3, 3, 2, 3, 1, 2]) # test([1, 2, 0, 1, 2, 2, 2, 0, 0, 2, 1]) # test([1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0]) # test([0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]) # test([0] * 21 + [2] + [0] * 21) # exit() def solve(n, aaa): # シェルピンスキーの三角形 if '2' not in aaa: factor = '3' effect = 2 else : factor = '2' effect = 1 # print(factor, effect) try_list = [] m = n - 1 while m: l = 1 << (m.bit_length() - 1 ) m ^ = l try_list.append((l, m)) # print(try_list) ans = 0 for i, a in enumerate (aaa): if a = = factor: for l, m in try_list: if i > = l: i - = l if i = = 0 : ans ^ = effect break elif i > m: break return ans n = int ( input ()) aaa = input () print (solve(n, aaa)) |
C - Giant Graph
問題
- N 頂点の単純無向グラフが3つある。X,Y,Z とする。
- いずれも各頂点に 1,2,...,N の番号が付いている
- それぞれのグラフの形状(辺数 M1,M2,M3 およびどの頂点とどの頂点が結ばれているか)は与えられる
- ここから、N3 頂点のグラフ W を作る
- W の1頂点は「X,Y,Z から1頂点ずつ選んだ状態」1つと対応する
- (Xi,Yj,Zk) を選んだ状態を、Wi,j,k とする
- 頂点 Xi,Xl 間に辺があるとき、全ての j,k で Wi,j,k,Wl,j,k 間に辺がある
- 頂点 Yj,Yl 間に辺があるとき、全ての i,k で Wi,j,k,Wi,l,k 間に辺がある
- 頂点 Zk,Zl 間に辺があるとき、全ての i,j で Wi,j,k,Wi,j,l 間に辺がある
- 頂点 Wi,j,k の重みを、1018(i+j+k) とする
- 2≤N≤105
- 1≤M1,M2,M3≤105
解法
なんかスケールが大きすぎてわけわかんなくなる問題。
解法もわりと発想がぶっ飛んでて、どうやったら思いつくのか、次に類題が出ても再現できるか怪しい。
とりあえず、W は、X,Y,Z の頂点をそれぞれ x,y,z 軸に1列に並べて、それを立方体状に複製したようなグラフとなる。
頂点の重みの決め方から、x,y,z 軸にどれか1つ増えるたびに重みは 1018 倍になる。
ある頂点を選択したことによって、それより小さな最大 3(N−1) 頂点が選択できなくなったとしても、その1頂点の重みには届かない。 よって、x+y+z の大きい方から貪欲に選んでいってよい。
つまり、番号の大きい頂点から貪欲に以下を繰り返せば、答えは出る。
- 辺で繋がった自分より番号の大きい頂点の内、採用したものが1つでもあれば、採用できない
- 採用したものが1つもなければ、採用する
しかし、これは N3 以上かかる。どうするか。
この貪欲な決め方は、ゲームの勝ち負けを問う問題と非常に似ている。
- 最終状態を「負け」とする
- 最終状態に近い方から貪欲に決めていく
- ある状態から遷移できる状態の内、「負け」が1つでもあれば、その状態は「勝ち」
- 全て「勝ち」にしか遷移できないのであれば、「負け」
さらに今回は、グラフは3次元的だが、辺は x,y,z 軸のいずれかと平行にしか引かれていない。 つまり、遷移先は x,y,z 軸のうち、いずれか1つに平行な方向しかない。
このような場合、勝ち負け状態の判定にGrundy数を使える。
- 最終状態のGrundy数を0とする
- ある状態のGrundy数は、そこから遷移できる状態のGrundy数に含まれない、最も小さな非負整数とする
これを X,Y,Z でそれぞれ単独に求めておけば、GWijk=GXi xor GYj xor GZk で求められる。
これが0ならば「負け」、つまり元の問題に立ち返ると「採用できる頂点」となる。
さて、では GWijk が0になる個数と重みを求めたい。愚直にやればやはり O(N3) かかる。
次に着目すべきは、X,Y,Z それぞれの中で、Grundy数が同じものはまとめられないか、という点である。
ある頂点のGrundy数を k にしようと思えば、当然、最低 k 個の遷移先がなければいけないし、その中に k−1 が無くてはいけない。 再帰的に考えると、合計、最低 k(k+1)2 本の辺が必要となる。 つまり、Grundy数の最大値は約 √2M 程度であることが分かる。
ならば、以下のようにまとめてしまえば、高速化できる。
たとえばグラフ X でGrundy数が“1”となる頂点の番号が3,4,7ならば、GX[1]=F3+F4+F7 とする。ただし F=1018 とする。
同様に GY,GZ も作り、GX,GYについて二重イテレートする。
X のGrundy数3(011)、Y のGrundy数5(101) ならば、Z に求められるGrundy数は6(110)である。
従って、GX[3]×GY[5]×GZ[6] を計算すれば、そのGrundy数の組み合わせによる答えへの寄与が求まる。
このようにして、Grundy数のxorが0となるような組を全て合計したものが答えとなる。
計算量は、最初にそれぞれでGrundy数を求めるのが O(N+M)、Grundy数ごとにまとめた結果を二重イテレートするのが O(√M2)=O(M) なので、十分高速である。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
import sys from collections import defaultdict n, * mabs = map ( int , sys.stdin. buffer .read().split()) MOD = 998244353 base = 10 * * 18 % MOD base_costs = [base] for i in range (n - 1 ): base_costs.append(base_costs[ - 1 ] * base % MOD) graphs = [] i = 0 for _ in range ( 3 ): m = mabs[i] i + = 1 j = i + 2 * m parents = [ set () for _ in range (n)] for a, b in zip (mabs[i:j: 2 ], mabs[i + 1 :j: 2 ]): a - = 1 b - = 1 if a > b: a, b = b, a parents[a].add(b) grundy = [ 0 ] * n weights = defaultdict( int ) for v in range (n - 1 , - 1 , - 1 ): pgs = {grundy[p] for p in parents[v]} for g in range (m + 1 ): if g not in pgs: grundy[v] = g weights[g] + = base_costs[v] break for g in weights: weights[g] % = MOD graphs.append( dict (weights)) i = j ans = 0 graph_x, graph_y, graph_z = graphs for gx, cx in graph_x.items(): for gy, cy in graph_y.items(): gz = gx ^ gy if gz in graph_z: ans = (ans + cx * cy * graph_z[gz]) % MOD print (ans) |
D - Merge Triplets
問題
- 正整数 N が与えられる
- 以下の操作によって生成される可能性のある数列 P の個数を求めよ
- 空の数列 P を用意する
- 1~3N の整数を、3つずつの N 個の数列 A1~AN に振り分ける
- 以下を 3N 回繰り返す
- A1~AN で空でないものの先頭の要素のうち、最小の要素を x とする
- x を Ai から削除し、P の最後尾に追加する
- 答えは、素数 M で割った値で求めよ(答えが M の倍数となることはない程度に大きい素数が与えられる)
- 1≤N≤2000
例
P A [8 1 4] [] [2 9 5] 8,2,6のうち、2が最小 [6 3 7] [8 1 4] [2] [9 5] 8,9,6のうち、6が最小 [6 3 7] [8 1 4] [2 6] [9 5] 8,9,3のうち、3が最小 [3 7] ... [2 6 3 7 8 1 4 9 5] この振り分け方では、←のような数列となった
解法
必要条件を丁寧に探していく。
「出来上がりの形としてあり得るものの個数」を問う問題は、 「ある順列 P があったとして、これを問題の条件で作ることは可能か?」という問題を解くのであって、 「A1~AN を列挙して再現して数え挙げる」という方針は悪手であることが多い。
道半ばの考察
N=3 として、1~9 を並べることを考える。長さ3の、3つの数列に振り分けることになる。
まず、置ける場所の制約がきつそうな、大きい数から考える。
最大の9は先頭付近に来られない。他の数列の先頭要素が何であろうと、9よりは小さいので、そちらが選ばれてしまう。 結局、9が数列の先頭にいたとしても、それ以外の2つの数列が全て空になるまでは、選ばれない。つまり、末尾3つのどこかにしか来られない。
[9 ? ?] [? ? ? ? ? ?] [] 自身以外の全てが空になって、初めて選べる [] 9が選ばれたら、残りは数列に残っている順番通りに取り出される
じゃあ、次に8。同様に、8も9も含まない数列が空になるまでは選ばれることはない。
- 9の後ろにあったら9が取り出され次第取り出される
- それ以外は、9がせき止めてる要素の個数より、3つ前までにしか来られない
[8 ? ?] [? ? ? ?] [9 ?] 9が自身を含め2要素せき止めていたら、 [] 8は末尾から5~3番目にしか来られない
せき止めている、とは、自身の後ろに連続する、自身より小さい数(ただし自身を含む)とする。せき止められている数は、先頭が取り出されると必ず続けて取り出される。 上記は、「4つ以上をせき止めている要素があってはいけない」と言い換えられる。
そう考えると、大きい方から置ける場所を確定させていって、ある数 k を置けるのは、k+1~3N の中で最も左に置いた位置より3つ左までのどこか、という方針で可能か?
正しい考察
上記は、一つ条件を見落としている。
せき止めている1つのまとまりを「ブロック」と呼ぶことにする。
「サイズ2個 or 3個のブロック」は、1つの配列に1つしか置けないので、N 個以上作れない。
つまり、以下のような順列は作れない。
[1 6 2 7 3 8 4 9 5] サイズ2のブロックが4個 > N=3 ~~~ ~~~ ~~~ ~~~ これを作るには、6が選ばれる直前に以下のような状態になってないといけないが、これはあり得ない [6 2] [1] [7 3] [8 4] [9 5]
サイズ3のブロックはどっちみち N 個以上作れないが、2のブロックは N より多く作れてしまうので、問題となる。
2個のブロックは1個のブロックと組み合わせないといけないので、1個のブロック数より多くなってはいけない。
よって、必要な条件は、以下の2つである。
- ブロックの長さは3以下
- 2個のブロックの個数は、1個のブロックの個数以下
この条件を踏まえて、どう数え挙げるか。
少ない要素数から、DPを行うことで可能となる。
- DP[i][j]=1~i の要素を使って、(1個のブロック数 - 2個のブロック数)が j の場合のパターン数
- 初期条件
- DP[0][0]=1
- DP[1][1]=1
- DP[2][2]=1
- DP[2][−1]=1
- 求める値
- j≥0 であるような DP[N][j] の総和
遷移は、ブロックの長さが3以下なので、末尾にそれぞれの長さのブロックを付与する場合を考える。
最後のブロックの先頭要素は必ず i となる。
1個
DP[i−1] から遷移する。最後に i の1個ブロックを加えるしかないので1通り。
- DP[i][j]+=DP[i−1][j−1]
2個
DP[i−2] から遷移する。P の最後の2個は「...,i,x」となるが、x に来る数は、1~i−1 のどれでもよい。
ここで DP[i−2] は、定義通りだと「1~i−2 までの数を使ってできるパターン数」だが、実際は要素の大小関係にしか意味が無いので、
- 1,2,3,...,i−3,i−2
- 1,2,3,...,i−3,i−1
- …
- 1,3,4,...,i−2,i−1
- 2,3,4,...,i−2,i−1
これらの i−1 通りのセットを使ってできるパターン数は、どれも同じ答えとなる。よって、末尾に来る数を決めれば、残りの並べ方はそれぞれが DP[i−2] 通りずつある。
- DP[i][j]+=DP[i−2][j+1]×(i−1)
3個
2個の場合と同様、末尾に来る2つの数の組み合わせは (i−1)(i−2) 通りあり、残りの並べ方は DP[i−3] 通りずつある。
- DP[i][j]+=DP[i−3][j]×(i−1)(i−2)
実装
j が負となりうるため、言語によっては適切にオフセットを設定してやる必要がある。
Pythonなら負のindexは末尾からのアクセスとなるため、配列を十分な長さで作っておけば、気にせずそのまま扱える。
また、j を±1する遷移があるが、numpyを使えば np.roll() で(-1→0 や 0→-1も含めて)簡潔に1つずらした配列を得られる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import numpy as np n, MOD = map ( int , input ().split()) n3 = n * 3 dp = np.zeros((n3 + 1 , int (n * 4.5 ) + 1 ), np.int64) dp[ 0 ][ 0 ] = dp[ 1 ][ 1 ] = dp[ 2 ][ 2 ] = dp[ 2 ][ - 1 ] = 1 for i in range ( 3 , n3 + 1 ): dp[i] + = dp[i - 3 ] * (i - 2 ) * (i - 1 ) dp[i] + = np.roll(dp[i - 2 ], - 1 ) * (i - 1 ) dp[i] + = np.roll(dp[i - 1 ], 1 ) dp[i] % = MOD print (dp[ - 1 ][:n3 + 1 ]. sum () % MOD) |