Loading [MathJax]/jax/output/CommonHTML/jax.js

目次

NECプログラミングコンテスト2021(AtCoder Beginner Contest 229) F,G,H問題メモ

NECプログラミングコンテスト2021(AtCoder Beginner Contest 229)

なぜかめっちゃ眠かった。

F - Make Bipartite

F - Make Bipartite

問題

解法

グラフに見せかけたDP。

グラフは車輪のような形になる。

  ①--②
 / \  / \
⑥--⓪--③
 \ /  \ /
  ⑤--④

二部グラフは、頂点を2グループに分け「同じグループの間には辺が張られていない」状態ならよい。

グループAとBに分け、頂点0の入る方をグループAと決めてしまうとして、 頂点 0i1 で成立している状態に i を加えることを考える。

ただし、i=1 のとき、2番目の条件は「1N が同じグループなら BN は削除」となる。
このループ部分(BN の辺)がややこしいので、頂点1に関しては2番目の条件は一端無視する。

頂点 i を加えるときの条件から、i1 の入ったグループがどっちだったかさえわかれば、削除しなければならない辺が決まる。

また無視していた BN については、DP[N] まで計算した後で 「頂点 1N をそれぞれどちらに入れたか」がわかれば、削除しなければならないかが決まる。

従って、そこでも場合分けしてやればよい。

最後に、DP[N][A][A] および DP[N][B][B] には BN を加算してやれば、全ての辺を考慮できたことになる。

DP[N] の中の4つの値の最小値が答え。

Python3

G - Longest Y

G - Longest Y

問題

解法

いくつかの気付きと前準備と、添字をバグらせない丁寧さが必要。

答えを二分探索できないか考える。
Yを x 個連続させられるかどうかを O(|S|) くらいで判定できれば可能そう。

x を固定して考える。

Y同士を入れ替える操作は無駄なので、S の中のYの並び順は最初から最後まで変わらないとしてよい。
S の中で i 番目に出現するYのことを Yi と表すとする。

連続させる一番左のYを Yi としたら、一番右は Yi+x1 に決まる。

この区間のYを連続させるためのswap回数を求めたい。

基準となるYを1個決める。 基準のYは動かさず、周囲のYを持ってくることを考える
Y..Y.YY....[Y].YY

基準Yから左右それぞれのYとの間にある'.'の個数を数えたら、その和がswap回数
Y..Y.YY....[Y].YY
7  5 44        11  →  7+5+4+4+1+1 = 22

ここで、たとえば基準を1個左にずらした時の変化量は、その間に'.'が k 個あったとして

なので、最適な基準位置は、区間中央のYであることがわかる。偶数の場合はどちらでもよい。

Y..Y.Y[Y]....Y.YY
3  1 0       4 55  →  3+1+0+4+5+5 = 18

従って、以下が前計算によりパッと求められると嬉しい。

愚直には各 Yi から各 Yj の間にある'.'の個数を数えた上で累積和を取っておけばよいのだが、 しかし2次元でテーブルを作ってしまうと O(|S|2) のためTLE or MLE。

そこで、基準を1個ずらしたときに法則性が無いかを確かめると、

      Y..Y.YY....Y.YY
基準     / | \   | | \
Y0   0  2  5  8 15 23 31
    (  -2 -4 -6 -8-10-12)
Y1      0  1  2  7 13 19
    (     -1 -2 -3 -4 -5)
Y2         0  0  4  9 14

Y3            0  4  9 14
    (           -4 -8-12)
Y4               0  1  2
...

ずらすごとに等差数列を引いた形になっている。
まぁ、ずらしたところから右にある各Yまでにある'.'の個数が一定数ずつ減るので、累積和を取ったら等差で減るのは当然っちゃ当然。

基準を Yi にする時に追加される等差数列の公差は、直前の Yi1 との間にある'.'の数であり、R(i,i) が0になるように位置を揃える。

以上の長さ O(|S|) の3つの配列を前計算しておけば、差分計算で R(i,j)O(1) で求められる。

L(i,j) も同様にすればよい。

すると区間 [l,l+x) のYを連続させるswap回数は、m を区間中点、r=l+x1 として、 L(m,l)+R(m,r) で求められ、これが K 以下なら可能となる。

x ごとに l を全通り試しても O(|S|) で判定でき、全体で O(|S|log|S|) となる。

Python3

解法2

上記のような七面倒くさいことしなくても、ちょっと発想を変えればもっと楽に計算できる。

公式Editorialの解法。二分探索は一緒。

S からYの箇所のindexを抜き出した配列を A とすると、1回のswapは、どれか1個を±1する操作に対応する。
(Y同士のswapは行わない前提として、異なる Ai が同じ値になる瞬間があってはいけない)

Y..Y.YY....Y.YY    A = (0  3  5  6 11 13 14)
      ×                        ↓+1
Y..Y.Y.Y...Y.YY    A = (0  3  5  7 11 13 14)

Yを連続させるというのは、A に連番をなるべく長く発生させるという問題に言い換えられる。

さらにこれは、あらかじめ Ai から i を引いて Bi とおくことで、「同じ値を」なるべく多く作る問題にできる。

Y....YYYY....YY    A = (0  5  6  7  8 13 14)
                   B = (0  4  4  4  4  8  8)

すると、[l,r) の区間のYを揃えるには、中央 m に揃えるのが最適となるので、

              ■■                 ↓↓      
Bm        ■■■■   →  ↑↑↑■■■■
        ■■■■■       ↑↑■■■■■
    ■■■■■■■       ■■■■■■■
    l     m       r      l     m       r

累積和を前計算しておけば操作回数を O(1) で計算できる。

H - Advance or Eat

H - Advance or Eat

問題文

制約

解法

非不偏ゲームの解析。知ってないと自力で思いつくのは厳しい。公式Editorialを読んでの個人的なメモ。

盤面最大 64 マスにつき「白、黒、何も無い」の3通りの状態を持つ 364 のDPは、 初期盤面から遷移可能な状態に絞ったところで、さすがに状態数が多すぎて無理。

Nimのように「どちらのプレイヤーも同じ手を打てる」ゲーム(不偏ゲーム)の場合は、 Grundy数などを使って効率的に解けることがあるが、今回はそうではない。

もし不偏ゲームなら、今回の問題では「各列は互いに干渉しない」ので、1列1列をNimの山のように個別に考えられそうなのに。。。

ところが、非不偏ゲームでも一定の条件を満たす場合は、Grundy数ほど簡単ではないが、1列ずつを個別に考える方法がある。

超現実数

スコアには超現実数(Surreal Number)というのを用いる。

分母が2冪に限定された実数であり、特に「2つの数に挟まれた数」を特定の方法で一意に決められるのが特徴。

{L|R} という表記は、以下の条件を満たす実数 x を示す。この条件から一意に決まる。

ゲーム木の構築とスコアの設定

今回の場合、ゲーム木の1頂点は「1列」の状態とする。
頂点から頂点へ、先手が可能な遷移は「白い辺」、後手が可能な遷移は「黒い辺」を張るとする。

各頂点に「スコア(評価値)」を割り当てるのだが、ここに超現実数を用いる。
以下の条件でスコア付けすることを考える。

そのためには、葉からスコアを決定する上で、以下ならよい。

先ほどの超現実数を用いて、{SW|SB} で表される実数を、その頂点のスコアとする。
(遷移できる白い辺が無い場合は、SW=、黒い辺が無い場合は SB= などとしておく)

全ての頂点でこの条件を満たすようにスコア付けすることができればよい。仮に、今はできたと仮定する。

c の現在の状態を vc とし、そのスコアを Svc で表す。
現在の盤面の全ての列のスコア合計 cSvcT とする。
T の正負は、以下のように推移する。

よって、「初期状態の T が正なら先手必勝、0か負なら後手必勝」となる。

(今回の問題では必要ないが)より細かく言うと、
  • T が正なら、「白い辺を使う方」の必勝
  • T が負なら、「黒い辺を使う方」の必勝
  • T が0なら、「後手」の必勝

となり、「白黒辺」による勝利と、「先手後手」による勝利の2種類がある。

T が正なら先手の操作後も必ず非負となる理由

たとえば「T=0.125=18」の時に、 「どの列の、どの白い辺の遷移も、Sv18 より大きく減少してしまう」状態になったら、 0を飛び越して一気に負にならざるをえない。

逆に、「T を既約分数にした時の分母が 2d(上例では8)なら、白い辺の遷移による減少は、必ず 12d 以下である」ことが示せればよい。

で、これは必ず言える。

T が正なら先手は必ず操作できる理由

T が正なら、当然、正である Sv が1つは存在する。

白い辺が出ていない、Sv>0 の頂点を考える。

Sv={SW|SB} という定義を考えたとき、白い辺が無いなら SW= である。
また、Sv が正なら SB も正ということになる。

しかし、それなら超現実数のルール上、Sv=0 となっているはずであり、矛盾する。

Sv が正なら、白い辺は必ず出ていることになる。

全ての頂点でスコアの条件を満たせる証明

どのようなゲームでもスコアを上手く設定できるわけではなく、 今回のゲーム設定がたまたま性質を満たしているので効率的に解ける、ということになる。

いま、状態 v のスコア Sv を決めるとして、そこから遷移できる全ての状態は既にスコアが矛盾なく求まっているとする。

v からの遷移先で、どのように白い辺 va と黒い辺 vb のペアを持ってきても、 遷移先のスコア Sa<Sb であることが示せればよい。

操作には「ある駒を進める」「ある駒を食べる」の種類があるが、

この2つがともに成り立つことを、「互いに干渉しない」とする。

互いに干渉しない場合、ゲーム木は以下のようになっており、

 ,--> a ==,     -->: 白い辺
v          w    ==>: 黒い辺
 `==> b --'

Sa<Sw<Sb なので、問題なく Sv を設定できる。

「互いに干渉する」のは、「先に行動する方が駒を先に食べると、後に行動する方はその駒を動かせなくなる」のみである。

その場合、例えば先手が先に行動するなら

vの状態  aの状態  bの状態
  ・       ・       ●        ・: 空きマス
  ●       ・       ・        ●: 黒い駒

 ,--> a     -->: 白い辺
v     ↑    ==>: 黒い辺
 `==> b

このように、状態 b からでも先手は黒を食べて a に遷移することができ、ba にも白い辺が張られていることになる。

よって、Sa<Sb が成り立ち、問題なく Sv を設定できる。

Python3