パナソニックグループ プログラミングコンテスト2025(AtCoder Beginner Contest 427)E,F,G問題メモ

E - Wind Cleaning

問題文

  • $H$ 行 $W$ 列のグリッドがあります。このグリッドの上から $i$ 行目、左から $j$ 列目のマスを $(i, j)$ と表記します。
  • 高橋君はこのグリッドの $1$ つのマスの上におり、グリッド上のいくつかのマスにはゴミが落ちています。
  • 各マスの状態は $H$ 個の長さ $W$ の文字列 $S_1, S_2, \ldots, S_H$ によって与えられ、$S_i$ の $j$ 文字目を $S_{i, j}$ として、以下のような状態を表します。
    • $S_{i, j} =$ 'T' のときマス $(i, j)$ は高橋君がいるマスである。
    • $S_{i, j} =$ '#' のときマス $(i, j)$ はゴミが落ちているマスである。
    • $S_{i, j} =$ '.' のときマス $(i, j)$ はゴミが落ちていない空きマスである。
  • また、高橋君がいるマスにはゴミは落ちていません。
  • 高橋君は、以下の操作を繰り返し行うことができます。
    • 上下左右のいずれかの方向を $1$ つ選び、すべてのゴミを同時にその方向へ $1$ マス移動させる。ただし、ゴミがグリッドの外に出た場合にはゴミは消滅し、高橋君のいるマスにゴミが移動した場合は高橋君は汚れる。
  • 高橋君が汚れることなくすべてのゴミを消滅させることができるか判定し、できる場合は操作を行う回数の最小値を求めてください。

制約

  • $2 \leq H, W \leq 12$
  • $S_i$ は長さ $W$ の 'T', '#', '.' からなる文字列
  • $S_{i, j} =$ 'T' なる $(i, j)$ がちょうど $1$ つ存在する
  • $S_{i, j} =$ '#' なる $(i, j)$ が$1$ つ以上存在する

解法

操作毎に「4方向のどちらにゴミを飛ばすか」の幅優先探索で解けるが、 問題設定の通りに全てのゴミを移動させ、その盤面をキーとしていると、状態が持たせづらい。
(持たせられないわけではないし、キーの更新・辞書からのキーの照合という定数倍を除けば、直に盤面を持っても計算量は変わらず解ける。 実装のしやすさからいうと、盤面持たせた方が楽だったかも)

「高橋君の方を移動させる」と考えても、相対的な高橋君とゴミの位置関係は変わらないので、 ゴミにぶつかるかどうかは管理できる。この時、高橋君は盤面の外にも移動できるとする。

ただ、「一旦ゴミを盤面から落とすと、次以降はそのゴミのマスを通過することができる」というのが曲者。

...#...
.......  一旦、上に動いて最下段のマスのゴミを落とし、
.......  次は、下に動いて元ゴミのマスを通過しつつ、最上段のゴミを落とすのがよい。
...T...
.#####.

BFSのためには、以下がわかるような状態をキーとしなければいけない。

  • 全てのゴミを落としたか?(ゴール判定)
  • 移動しようとしているマスが '#' だったとき、そのゴミは残っているかいないか?

$(u,d,l,r,i,j)$ をBFSのキーとする。

  • $u,d$: 高橋君が訪れたことのある最小・最大の $x$ 座標
  • $l,r$: 高橋君が訪れたことのある最小・最大の $y$ 座標
  • $i,j$: 高橋君の現在位置

高橋君の初期位置を $(t_x,t_y)$ とする。

  • 最下段を $d$ まで移動していたとすると、$i \lt d-t_x$ にあるゴミは全て落とされている。
  • 最上段を $u$ まで移動していたとすると、$i \ge H-(t_x-u)$ にあるゴミは全て落とされている。

左右についても同様に、$u,d,l,r$ の情報で落とされているゴミが分かるので、BFSの更新・ゴール判定ができる。

キーの各値は $-H~H, -W~W$ の範囲を動く(それだけ動いたら確実に全てのゴミは落とされている)。
また、ゴール判定には $O(HW)$ の計算量がかかる(二次元累積和で $O(1)$ にすることもできる)。

よって、全体で $O(H^4W^4)$ で解ける。
実際には、「上と下の両方ともに大きく動くより前に答えが見つかる」などで何分の一かになることが期待され、間に合う。

Python3

F - Not Adjacent

問題文

  • 長さ $N$ の整数列 $A=(A _ 1,A _ 2,\ldots,A _ N)$ が与えられます。
  • $A$ の(連続するとは限らない)部分列は $2 ^ N$ 通りあります。
  • 部分列 $(A _ {i _ 1},A _ {i _ 2},\ldots,A _ {i _ k})\ (1\le i _ 1\lt i _ 2\lt\cdots\lt i _ k\le N)$ であって、次の $2$ つの条件の両方を満たすものがいくつあるか求めてください。
    • 選ばれた要素は $A$ において隣接しない。つまり、すべての $1\le j\lt k$ に対して $1+i _ j\ne i _ {j+1}$ が成り立つ。
    • 総和が $M$ の倍数である。つまり、$\displaystyle\sum _ {j=1} ^ kA _ {i _ j}\equiv0\pmod M$
  • $2$ つの部分列が整数列として等しくても、取り出す位置が異なるならそれら $2$ つの部分列を区別して数えることに注意してください。

制約

  • $1\le N \le 60$
  • $1\le M \le 10 ^ 9$
  • $0\le A _ i\lt M$
  • 入力はすべて整数

解法

$M$ がでかいので「$\mathrm{DP}[i,d]:=A_i$ まで考慮して $\mod{M}$ が $d$ の状態の数」みたいな、 状態数が $O(M)$ 以上かかるDPはできない。

実は、ただ問題を難しくするために設定されているような1つめの条件(隣接しない)が重要で、 この条件があることで半分全列挙で解けるようになっている。

この条件が無いと、数列の半分 $N/2=30$ の取り得る部分列の個数は $2^{30} \simeq 10^9$ で多すぎるのだが、 隣接しないという条件があるとその個数はフィボナッチ数となることが知られており、 $\mathrm{fib}_{32} = 2178309$ 以下となる。 (数列の始めをどのようにするかで、A000045 - OEIS の定義より2個ずれる)

「末端要素を使うか」で分けて左右から半分ずつDPし(右半分のDPは右端から始める)、 最後に「末端を使ったもの同士」以外で合わせると、$O(\mathrm{fib}_{N/2})$ で解ける。

Python3

G - Takahashi's Expectation 2

問題文

  • 高橋くんは、これからいくつかのプレゼントを1つずつ順番にもらう予定です。
  • 高橋くんにはテンションという整数のパラメータがあり、テンションはプレゼントをもらうごとに以下のように変動します。
    • もらったプレゼントの価値 $P$ がテンションの値以上であるとき、高橋くんはプレゼントに喜び、テンションが $A$ だけ増加する。
    • もらったプレゼントの価値 $P$ がテンションの値未満であるとき、高橋くんはプレゼントにがっかりし、テンションが $B$ だけ減少する。
  • はじめ、高橋くんが受け取る予定のプレゼントは $N$ 個あり、$i$ 番目 $(1\le i\le N)$ に受け取るプレゼントの価値は $P _ i$ です。
  • $2$ 種類からなる $Q$ 個のクエリが与えられるので、その全てを順に処理し、すべての質問クエリに答えてください。
    • $1 \ X_i$(追加クエリ): 受け取る予定のプレゼントの末尾に、価値が $X _ i$ であるプレゼントを新たに追加します。
    • $2 \ X_i$(質問クエリ): 次の質問に答えてください。
      • 高橋くんのテンションがはじめ $X _ i$ だったときの、受け取る予定のプレゼントをすべて受け取ったあとの高橋くんのテンションを求めよ。

制約

  • $1\le N\le2\times10 ^ 5$
  • $1\le A\le10 ^ 9$
  • $1\le B\le10 ^ 9$
  • $-10 ^ 9\le P _ i\le10 ^ 9\ (1\le i\le N)$
  • $1\le Q\le2\times10 ^ 5$
  • 追加クエリならば $-10 ^ 9\le X _ i\le10 ^ 9\ (1\le i\le Q)$
  • 質問クエリならば $-10 ^ {12}\le X _ i\le10 ^ {12}\ (1\le i\le Q)$
  • 入力はすべて整数

解法

まず「プレゼントが末尾に追加されるたびに、どういう更新処理をすればよいか」の考察が難しく、また「それを実現するデータ構造」も実装が重たい。

更新処理

まず、テンションの変化を以下のように言い換える。

  • プレゼントの価値がテンション以上なら変化せず、未満なら $A+B$ 下がる。
  • 最後に、無条件に(もらったプレゼントの個数 $\times A$)だけ上がる。

各 $P_i, X_i$ は、元の値から(直前まででもらっているプレゼントの個数 $\times A$)だけオフセットする必要がある。
以下の $P_i,X_i$ は、オフセット済みの値である前提とする。

その上で、以下のグラフ $y=f(x)$ を考える。

  • $f_i(x):=i$ 番目のプレゼントまでもらった段階で、高橋君の初期テンションが $x$ だったときの最終テンション

初期状態は $f_0(x)=x$ であり、/に無限に続くグラフである。

$f_{i-1}→f_{i}$ の更新について考えると、$f_{i-1}$ が $P_{i}$ より大きい部分について、$A+B$ だけ下がる。

            /
          /
P_______/___   →           /   __
      /              /|  /     |
    /              /  |/       ↓A+B
  /              /

これを繰り返していくと、何となく、どんどんバラバラなグラフとなり、 最悪 $2^N$ 個くらいの線分に分かれてとても管理できなくなる予感がするが、、、

だが、実際は意外と綺麗にまとまる性質がある。

         a       /
        ↓     /                      前のプレゼントで下がっていた b と
P_______/|__/__  →              /  今回のプレゼントで下がった a は
      /  |/          /|  /|  /    下がった先で綺麗に繋がり、
    /     ↑        /  |/  |/      1本の線分となる。
  /        b      /     ↑↑
                           a b

さらに、元の「/|」の位置がずれていても、そこをぶった切るような $P$ が来ると、綺麗に同じ高さのループにまとまる。 ループ回数(高さ $P$ まで上ってきた時に $A+B$ 下がる回数)は、ぶった切った「/|」の数 $+1$ となる。

                          /
                 /|    /
P_______/|____/__|__/____  →                           /
      /  |  /    |/              /|    /|    /|    /
    /    |/                     /  |  /  |  /  |  /
  /                            /    |/    |/    |/
                                       ~~     ~~~~   ~~~~~~~~ 今回下がった箇所

ただし、$P$ と比べて完全に上・完全に下であるような「/|」は、上下の平行移動のみで変化しない。

                 /|  /
               /  |/
P____________/_________  →                /|  /
           /                        /|  /  |/
    /|  /                   /|  /  |/
  /  |/                   /  |/
                                        ~~~~~~~~~~~ 今回下がった箇所

以上より、「グラフが下がる時は、必ず $A+B$ だけ下がる」ことが保証されるので、 $f(z)$ は「$x$ 座標が $z$ 未満の位置で $A+B$ だけ下がった回数」を $k$ とすれば、$z-(A+B)k$ として求められる。

よって、例えば以下の情報があれば、グラフの情報を保持できる。

  • 「ピーク(/| の上側の先っぽ)の存在する $x$ 座標」と「そこでのループ回数」
    • 同じ高さでループする場合は最も左のピークのみを記録する。
                                   /|  /
(例)                 /|  /|  /  |/
                     /  |/  |/    :
    /|  /|  /|  /    :           :
  /  |/  |/  |/      :           :
      :                  :           :
    (x1,3)             (x2,2)      (x3,1)

追加クエリでは、以下の処理ができれば情報を更新できる。

  • $y$ 座標が $P$ 以上 $P+A+B$ 以下にあるピークと、それらのループ回数の総和 $L$ を求める。
  • それらのピークを全て削除する。
  • 新たに「はじめて $y=P$ に到達する $x$ 座標 $z$」を求め、$(z,L+1)$ を追加する。

$y=P~P+A+B$ の範囲内にあるピークが、新たな $P$ により“ぶった切られる”。 これらは全て高さ $P$ でのピークに揃えられるので、1つのエントリにまとめなおせばよい。

質問クエリでは、以下の処理ができれば答えが求められる。

  • $x$ 座標 $[-\infty, X_i)$ に存在するピークの個数 $k$ を求める。
  • $X_i-(A+B)k$ が答え

データ構造

追加クエリでは $y$ 座標を基準に、質問クエリでは $x$ 座標を基準に、区間和を求めるという処理が必要となる。
さらに追加クエリではエントリの削除も必要となる。

さすがに無茶だろ、と思わなくもないが、以下の性質から可能となる。

  • 記録される $x$ 座標と、それに対応する $y$ 座標は、どちらも狭義単調増加が保証される。
    • つまり、$y$ の値で区間を成す範囲は $x$ 座標をキーにしても区間を成している。
  • $x$ に対応する $y$ の値は、前述の通り $x$ 未満の位置のピークの個数から計算できる。

$x$ 座標をキー、ループ回数を値とした区間和セグ木を使う。 ただし、キーの取り得る範囲が広いにもかかわらず先読みできないため、動的セグ木で管理する。

ここで、「$x$ でなく $y$ 座標をキーにすることもできるのでは?」とも思うが、$y$ 座標での管理は難しい(と思う)。 既存のエントリに対して「一定の値以上の $y$ 座標は全て $A+B$ ずつ下がる」という変更がプレゼントのたびに発生するが、 キー自体に変更が生じるこの処理が上手く実現できない。 $x$ 座標なら、更新クエリの削除以外でキー自体の変更は発生しないため、管理しやすい。

追加クエリでは、以下を処理したい。

  • bisect(p): $y$ 座標が $p$ 以上となる最小のキーと、そのキー未満のループ回数の総和
  • delete(l, r): $x$ 座標が $[l,r)$ の範囲にあるエントリを削除する
  • add(x, loop): エントリ $(x,loop)$ を追加する

bisect が実装できれば、範囲境界の $y=P, P+A+B$ 付近に対応する $x$ 座標が得られる。
それを使えば、区間和を求めること、delete, add はそこまで難しくない。

bisect は、セグ木の各ノードに「自身以下最大のキー」と「そのキーに対応する値」を追加で持たせておく。
また、根から、先頭からの累積和 prefix を管理しながら辿っていく。
$x$ に対応する $y$ 座標を計算して $p$ 未満かどうかを返す関数 $f(x,prefix)$ を定義しておけば、

  • 左ノードに存在する最後のエントリの $y$ 座標が $p$ 未満か?を $f$ でチェック
    • Yes→右ノード探索、prefix に左ノードの総和を加算
    • No→左ノード探索

のようにして二分探索が可能となる。

Python3

programming_algorithm/contest_history/atcoder/2025/1011_abc427.txt · 最終更新: by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0