geam1113’s diary

主にAtcoderのコンテストの備忘録を書きます。AtCoder緑と水色を行ったり来たりしています。

ABC246 参加記録

コンテスト中AC:A〜C,E
D,Fも自力ACできたので、書きます。

コンテスト全体の感想

Dが思いつかなかったので、Eを先に解きました。Eは解法自体はわかったのですが、実装に時間がかかってしまいました。

Fで残り時間10分くらいで、包除原理はわかったのですが、実装は間に合いませんでした。コンテスト後に解きましたが、最初S_iから作られる文字列の数の計算方法が間違っていました。

Dのように変数か複数ある場合、どれかを固定するという考え方が大事ですね。(一方は全探索できて、片方が決まれば、他は高速に計算できる場合がある)

D - 2-variable Function

aの上限は10^6で、全探索可能です。aを固定すると、b=0,1,\cdotsに対してXは狭義単調増加です。よって、X\leq Nとなるようなbを二分探索できます。

提出コード

E - Bishop 2

マスを4倍に拡張し、左上、右上、左下、右下への移動中を表す状態を追加します。具体的には、

dp[i][j][k]:=マス(i,j)にいて状態がkの時の最短距離

とします。
但し、k=\{0,1,2,3\}とし、
k=0の時、左上に移動中
k=1の時、右上に移動中
k=2の時、左下に移動中
k=3の時、右下に移動中
とします。

まず、同じマス内ではコスト1で方向転換ができます。(同じ向きになるのは無駄ですが、実装が楽です。) 従って、k'=0,1,2,3について、

dp[i][j][k']\leftarrow dp[i][j][k]+1

次に、kに応じて、以下の通りに進行方向の次のマスにコスト0で動けます。

  • k=0の時、左上に進める。
    dp[i-1][j-1][k] \leftarrow dp[i][j][k]

  • k=1の時、右上に進める。
    dp[i-1][j+1][k] \leftarrow dp[i][j][k]

  • k=2の時、左下に進める。
    dp[i+1][j-1][k] \leftarrow dp[i][j][k]

  • k=3の時、右下に進める。
    dp[i+1][j+1][k] \leftarrow dp[i][j][k]

初期値は、既に1回目の移動が始まっていることにする必要があるので、k=0,1,2,3について、

dp[A_x][A_y][k] \leftarrow 1

です。

これをダイクストラ法で解けば良いです。

答えは、k=0,1,2,3について、dp[B_x][B_y][k]の中の最小値です。
なお、到達可能かの判定が必要なことに気をつける必要があります。

実装ですが、AtCoderの過去問にも同様の問題があったので、その時の公式解説の実装方法に倣っています。
マス(i,j)の状態kの頂点番号を

id = 4(i+j*N)+k

とします。
idからi,j,kを得たい時は、C++なら、

i = id/4%N
j = id/4/N
k = id%4

とすれば良いです。
このようにするメリットは、通常のダイクストラ同様に、(距離, 頂点番号)という組だけで管理できる点です。i,j,kを全て管理するなら、構造体などが必要です。
提出コード

F - typewriter

包除原理で解けます。
包除原理についてはネットで調べると出てくるので、詳細は割愛します。
ここでは、小さい例で示します。

f(S)を使用する文字の集合Sのみから得られる長さLの文字列の数とします。
g(S)を使用する文字の集合Sから得られる全ての長さLの文字列の数とします。

入力例1でいえば、
f(S_1)=\{ab,ba,bb\}
f(S_2)=\{ac,ca,cc\}
f(S_1 \cap S_2)=\{aa\}

g(S_1)=\{aa,ab,ba,bb\}
g(S_2)=\{aa,ac,ca,cc\}
g(S_1 \cap S_2)=\{aa\}

g(S)は、実は簡単に求められます。Sに含まれる文字は何回使っても良いので、i番目の文字は|S|通りから選ぶことになり、選ぶ文字によって得られる文字列は異なるので、

g(S)={|S|}^Lとなります。これは繰り返し二乗法により、O(logL)で計算できます。

(とはいえ、g(S)の求め方がコンテスト中悩んでしまいました。重複組み合わせと重複並び替えを組み合わせようとしてしまっていました。)

では、N=3のケースで、包除原理の一例を示します。
今求めたいものは、

ans = f(S_1)+f(S_2)+f(S_3)+f(S_1 \cap S_2)+f(S_1 \cap S_3)+f(S_2 \cap S_3)+f(S_1 \cap S_2 \cap S_3)

です。ベン図で他の集合と重なっていない部分の和と考えればわかりやすいです。

初め、X=0とします。

まず、f(S_1),f(S_2),f(S_3)を足したいです。

g(S_1)=f(S_1)+f(S_1 \cap S_2)+f(S_1 \cap S_3)+f(S_1 \cap S_2 \cap S_3)
g(S_2)=f(S_2)+f(S_1 \cap S_2)+f(S_2 \cap S_3)+f(S_1 \cap S_2 \cap S_3)
g(S_3)=f(S_3)+f(S_1 \cap S_3)+f(S_2 \cap S_3)+f(S_1 \cap S_2 \cap S_3)

より、Xg(S_1),g(S_2),g(S_3)を足します。すると、

X=f(S_1)+f(S_2)+f(S_3)+2f(S_1 \cap S_2)+2f(S_1 \cap S_3)+2f(S_2 \cap S_3)+3f(S_1 \cap S_2 \cap S_3)

次に、f(S_1 \cap S_2),f(S_1 \cap S_3),f(S_2 \cap S_3)は一個ずつ多いので減らしたいです。

g(S_1 \cap S_2)=f(S_1 \cap S_2)+f(S_1 \cap S_2 \cap S_3)
g(S_1 \cap S_3)=f(S_1 \cap S_3)+f(S_1 \cap S_2 \cap S_3)
g(S_2 \cap S_3)=f(S_2 \cap S_3)+f(S_1 \cap S_2 \cap S_3)

より、Xからg(S_1 \cap S_2),g(S_1 \cap S_3),g(S_2 \cap S_3)を引きます。

X=f(S_1)+f(S_2)+f(S_3)+f(S_1 \cap S_2)+f(S_1 \cap S_3)+f(S_2 \cap S_3)

最後に、f(S_1 \cap S_2 \cap S_3)が足りなくなったので、足したいです。

g(S_1 \cap S_2 \cap S_3)=f(S_1 \cap S_2 \cap S_3)

より、Xg(S_1 \cap S_2 \cap S_3)を足します。

X = f(S_1)+f(S_2)+f(S_3)+f(S_1 \cap S_2)+f(S_1 \cap S_3)+f(S_2 \cap S_3)+f(S_1 \cap S_2 \cap S_3)

この時点で、X=ansとなります。

結局、
ans
= f(S_1)+f(S_2)+f(S_3)+f(S_1 \cap S_2)+f(S_1 \cap S_3)+f(S_2 \cap S_3)+f(S_1 \cap S_2 \cap S_3)
= g(S_1)+g(S_2)+g(S_3)-g(S_1 \cap S_2)-g(S_1 \cap S_3)-g(S_2 \cap S_3)+g(S_1 \cap S_2 \cap S_3)

となります。

これを一般化すると、g(S)の積集合Sに含まれるS_1,S_2, \cdots ,S_Nの個数が奇数なら足す、偶数なら引くということになります。

解法に移ります。

S=\{S_1,S_2,\cdots S_N\}とします。

  1. ans=0とする
  2. S空集合を除く全部分集合Tについて、3,4を行う
  3. Tに含まれるS_iの積集合を、Uとする
  4. Tの要素数が奇数ならansg(U)を足す。偶数なら引く

S_iにおける使用可能なアルファベットの集合はビットによる冪集合で管理すればよいです。
Sの全部分集合もビット全探索すればよいです。

Tに含まれるS_iの数や、Uに含まれるアルファベットの数はpopcountで得られます。

計算量は、部分集合の全探索に2^N、全探索中のそれぞれについて積集合を求めるのにO(N)、積集合から得られる文字列の数を求めるのにO(logL)なので、全体で多分(N+logL)2^Nです。
提出コード

ABC245 参加記録

コンテスト中AC:A〜C
コンテスト後解説なしでE,FをAC。
Dは解説見たが、方針は間違っていなかったが、考慮できていない部分があった。

D - Polynomial division

C_i = A_0 B_i + A_1 B_{i-1}+\cdots +A_i B_0

であるので、

i=0の時、B_0 = \frac{C_0}{A_0}

i\geq 1の時、B_i = \frac{C_i - (A_1 B_{i-1}+\cdots +A_i B_0)}{A_0}

として、B_0, B_1,\cdots ,B_Mの順で求められると考えました。
(なお、i\gt Nの時はA_i=0としておけば良い)

しかしながら、これはREになっていました。
なぜかというと、A_0 = 0の場合があるからでした。

そこで、以下のように考えました。
A_i \neq 0となる最小のiposとします。

C_{pos} = A_{pos} B_{0}

C_{pos+1} = A_{pos+1} B_{0} + A_{pos} B_{1}

C_{pos+2} = A_{pos+2} B_{0} + A_{pos+1} B_{1} + A_{pos} B_{0}

\cdots

となるので、以下の方針としました。

i=posの時、B_0 = \frac{C_0}{A_{pos}}
i\geq pos+1の時、B_{pos-i} = \frac{C_{i} - (A_{i} B_{i-1}+\cdots +A_{pos} B_0)}{A_{pos}}

とします。 しかし、この解法でもREとなり、時間内にACできませんでした。

コンテスト後に考えた結果、実装時に色々と考慮できていなかった部分があったことが原因であり、解法自体は正しいことがわかりました。

以下がACしたコードです。

提出コード

上の実装に、n = max(N,M)として、A,B,Cについて、それぞれ2n-1個分の領域を確保する処理がありますが、ここは不要です。

全てN+M個分確保しておけば良いです。逆にこれより少なくとだめです。
なぜなら、A_N\neq 0のみが保証されているため、iは最大でN+Mになるからです。

また、今回、Aのインデックスをベースに実装しましたが、Bをベースにした方がわかりやすいと思います。
提出コード

E - Wrapping Chocolate

小さいチョコレートから優先して箱に入れる場合、大きい箱に入れると、後々入れられないチョコレートが出てくる可能性があります。そこで、チョコレートを大きいものから順に入れることを考えます。

ここでチョコレートの大小関係を定義しておきます。チョコレートを縦を第一キー、横を第二キーとして降順ソートしたときの基準を適用します。すなわち、i番目のj番目のチョコレートについて、

  • A_j \lt A_iの場合はi番目が大きく、A_j = A_iの場合はB_j \lt B_iならi番目が大きい

となります。

まだ箱に入れられていないチョコレートの中で最大のものをi番目とします。

まず、縦を考えます。
A_i \leq C_jを満たす箱jに入れることが可能です。ここで、これを満たす箱であれば、どれに入れても、縦の制限によって箱に入れられないチョコレートが発生することはありません。なぜなら、残ったチョコレートの縦はA_i以下だからです。

次に横を考えます。
横については、B_i \leq D_jを満たす箱jならどれでもよいというわけにはいきません。なぜなら、残ったチョコレートの横はB_i以下であるとは限らないからです。
ということから、横はB_i \leq D_jを満たす中で最小のものにしておく必要があります。

以上をまとめると、以下のような貪欲法になります。

1.箱に入っていないチョコレートの中で最大をものを1つ選びチョコレートiとする
2.チョコレートが入っておらず、かつ、チョコレートiが入れられる箱の中で、横が最小の箱jを選ぶ
3.チョコレートiを箱jに入れる

具体的はアルゴリズムに移ります。

順序付き多重集合Sにチョコレートiを入れる候補となる箱を記録することにします。Sの順序はDの昇順とします。

Sには、チョコレートiを入れたい時、A_i \leq C_jを満たす箱jを全て記録します。その中から、B_i \leq D_kを満たす中で最小の箱kを二分探索によって探し、Sから削除します。

なお、Sに入っている箱は全てA_i \leq C_jを満たすので、C_jは記録する必要がなく、D_jだけ記録しておけばよいです。

これを素直に実装すると、各チョコレートについて、候補となる箱を探すため、集合への追加を除いてもO(NM)となり、間に合いません。

そこで以下の方法とします。

まず、チョコレートと箱はそれぞれ縦(A_i,C_i)を第一キーに、横(B_i,D_i)を第二キーとして降順ソートします。

i\lt i'の時、A_{i'} \leq A_{i}なので、A_i \leq C_jである時、A_{i'} \leq C_j となります。

そのため、チョコレートiSに追加した箱は、チョコレートi'でも追加したままにしておけます。

従って、i=1,2,\cdots ,Nの順に調べる時、 チョコレートiSに追加された箱は、チョコレートi+1でも追加したままにでき、チョコレートi+1で新たに必要になった箱だけをSに追加すればよくなります。

これにより、箱はSに高々1回しか追加されず、全体でO(N)となります。

多重集合としてC++のmultisetを使えば、追加、検索、削除はlog時間でできるので、高速に実現できます。

提出コード

F - Endless Walk

頂点vを出発して、頂点vに帰って来れるような閉路が存在すれば、vに到達後に無限に移動できることが確定します。

ある強連結成分C_i = \{v_{i_1},v_{i_2},\cdots v_{i_m}\}に属する全ての頂点は、この条件を満たします。

よって、強連結成分の集合族をC=\{C_1,C_2,...,C_m\}としたとき、これら強連結成分に属するいずれかの頂点に到達できれば、無限に移動できます。

強連結成分は、強連結成分分解でO(N+M)で得られます。AtCoder Libraryのscc_graphを強連結成分が二次元配列で得られるので、列のサイズが2以上であれば閉路を含み、C_iとみなせます。

(補足:グラフが単純でなければ、自己ループを考慮する必要があるので、サイズが1でも閉路となります。)

あとは、強連結成分の頂点に到達可能な頂点を調べれば良いですが、以下のように言い換えました。

  • Gの有向辺を全て逆向きにしたグラフG'において、強連結成分のいずれかの頂点から到達可能な頂点を調べる

これは強連結成分に属する全ての頂点をキューに入れてBFSを開始すれば良いです。

提出コード

ABC244 参加記録

コンテスト中AC:A〜E

C - Yamanote Line Game

インタラクティブな問題を初めて解いたのでメモ
提出コード

D - Swap Hats

自信ないまま通してしまいました。

操作を偶数回行った後に生成される状態には10^{18}回行った後もその状態にできます。

10回操作すると状態数は2^{10}になるものの、RGBの順番の入れ替えの通り数は6通りしかありません。
そのため、Sを10回操作してTにできれば達成できると考えました。

実際に実装してデバッグしてみると、状態数は偶数回、奇数回共に早々に3個に限定され、解を得られそうだとわかりました。
そして、提出した結果ACできました。

公式解説によれば、6種類の状態の関係性をグラフにすると、二部グラフになり、S,Tが同一のグループになれば達成できるということでした。

提出コード

E - King Bombee

パスに関する以下のDPを考えます。

dp[u][k]:=k回移動した後に頂点uにいるようなパスの総数
初期値はdp[S][0] = 1

これは、A=\{A_0,A_1,\cdots ,A_k\}のうち、A_k =uであるものの総数に一致します。

このDPテーブルの更新は、以下のような3重ループになります。

for k = 1からKまで
    for u in 全ての頂点
        for v in uと接続する全ての頂点
            dp[v][k] = dp[u][k-1] //uからvに移動する

for v in uと接続する全ての頂点の部分は、kのループの1回につき、合計で2M回しか実行されないので、O((N+M)K)になります。
また、K回目でTにいるパスの総数はdp[T][K]となります。

ここまでは、同様の考え方が過去に出題されました。

あとはXに訪れた回数(Aに含まれるXの個数)の偶奇の情報が欲しいので、DPテーブルに加えます。

dp[u][k][x]:=k回移動した後に頂点uにいて、かつXに訪れた回数の偶奇がxであるようなパスの総数
但し、x=\{0,1\}であり、x=0の時はXに訪れた回数が偶数、x=1の時は奇数

DPテーブルの更新は、以下のようにv=Xの時だけ、偶奇を入れ替えればよいです。

for k = 1からKまで
    for u in 全ての頂点
        for v in uと接続する全ての頂点
            if vがX
                dp[v][k][0] = dp[u][k-1][1]
                dp[v][k][1] = dp[u][k-1][0]
            else
                dp[v][k][0] = dp[u][k-1][0]
                dp[v][k][1] = dp[u][k-1][1]

答えはdp[K][T][0]です。
提出コード

ARC137 参加記録

コンテスト中AC:なし
A,B解説AC

A - Coprime Pair

公式解説

素数の間隔(prime gap)が重要とのことです。

素数の間隔 - Wikipedia

公式解説及び上記のwikiによれば、問題の制約における素数間の極大の間隔(maximum gap)は1500らしいです。

よって、L\leq x \leq L+1500及びR-1500 \leq y \leq Rを満たす組(x,y)のうち少なくとも1つはx,yがいずれも素数である組が存在するということでした。

d=y-xとすると、R-L-3000\leq d \leq R-Lの範囲だけgcd(x,y)=1となる組を調べれば良くなり、全探索可能になるということでした。

提出コード

B - Count 1's

  • コンテスト中にたどり着いた部分

連続した部分列の0の個数をS^0、1の個数をS^1、初期状態における1の個数をXとすると、操作後の1の数をX+S^0 - S^1にできます。

また、S=S^0 - S^1の最大値をS_{max}とし、その時の連続した部分列の区間を閉区間[l,r]とします。

区間が空の状態から、+1ずつ伸長して[l,r]にした時、Sも+1ずつしかされないので、S=0,1,\cdots ,S_{max}にできます。

最小値S_{min}についても同様です。
従って、Sは、S_{min}\leq S \leq S_{max}を満たす全ての整数にすることができます。

以上から、S_{min}\leq S \leq S_{max}に0を含むならS_{max} - S_{min} +1が答えとなります。
0を含まないなら、空の部分列を選ぶと0にもできるので、S_{max} - S_{min} +2となります。

  • コンテスト中わからなかった部分

S_{min},\ S_{max}の求め方がわかりませんでした。
公式解説によれば簡単に求められるとなっていました。

区間[1,i]の部分列に含まれる0の個数をS^0_i、1の個数をS^1_iとします。

[i,j]に含まれる0の個数はS^0_i - S^0_{j-1}、1の個数はS^1_i - S^1_{j-1}です。
よって、[i,j]S^0 - S^1は以下のように計算できます。
S^0_i - S^0_{j-1} - (S^1_i - S^1_{j-1}) = S^0_i - S^1_i - (S^0_{j-1} - S^1_{j-1})

従って、iを末尾とする連続した部分列のS^0 - S^1が最大となるのは、0\leq j \lt iを満たすjのうち、S^0_j - S^1_{j}が最小となるもの選んだ場合です(便宜上、 S^0_0 - S^1_0= 0)。
同様に、S^0 - S^1が最小となるのは、S^0_j - S^1_{j}が最大となるもの選んだ場合です。

よって、i=1,\ 2,\ \cdots ,\ Nについて、S^0_i - S^1_{i}を順次計算し、最大値と最小値を管理しておくことで、O(1)で各iを末尾とした時の連続した部分列のS^0 - S^1の最大・最小を求められます。

以上から、S_{min},S_{max}O(N)で求められます。

提出コード

ABC242G Range Pairing Query

解説AC

問題へのリンク
解説へのリンク

初Mo's Algotithm
Mo's Algorithmの適用可能条件は以下の通りです。

  1. 配列が不変
  2. クエリ先読み可
  3. 区間を+1, -1ずつ伸縮しても値の再計算が容易

1,2は満たすので3が満たせればよく、以下の方法で容易に計算できることがわかります。

cnt[i]:区間内のiの出現数
とします。最大のペア数ansは、以下のように計算できます。

  • 区間を伸長した時
    新たに区間内となるものをA_iとすると、伸長前にcnt[A_i]が奇数なら、ペアを新たに作れるのでansを+1する。

  • 区間を短縮した時
    新たに区間外となるものをA_iとすると、短縮前にcnt[A_i]が偶数なら、ペアが1つ減るのでansを-1する。

よって、Mo's AlgorithmによりO((N+Q\sqrt{N})で解けます。

Mo's Algorithmは公式解説のリンクなど見れば良いですが、自分でも別記事に簡単にまとめました。
Mo's Algorithm

提出コード

Mo's Algorithm

AtCoder Biginner Contestに出題されたので、実装してみました。

解説へのリンク(ネタバレ注意)

解説に書かれている以下のリンクに詳細があるので、アルゴリズムについては要約して書きます。

概要

数列A=\{A_0, A_1,\ \cdots ,\ A_{N-1}\}についてのQ個の閉区間[l_i,\ r_i]のクエリにO((N+Q)\sqrt{N})で答える。
やっていることはクエリの平方分割。

条件

  • Aは不変
  • クエリ先読み可
  • [l_i,\ r_i]から区間を伸縮しても、値を簡単に計算できる

アルゴリズム

  • ブロック番号\lfloor \frac{l_i}{\sqrt{N}} \rfloorの昇順、同ブロック内ではr_iの昇順でクエリをソートする
  • ソート済みのクエリを左から順に区間を伸縮して値を計算していく

計算量

  • 左端はほとんどがブロック内での移動なので、Q\sqrt{N}
  • 右端は同ブロック内で高々N回の移動なので、N\sqrt{N}
  • 上記を併せてO((N+Q)\sqrt{N})

Aのサイズを9とします。
ブロックサイズb=\sqrt{9}=3

与えられるクエリ
[5,7]
[1,3]
[3,5]
[4,4]
[7,8]
[2,5]
[0,8]
[7,7]

まず、クエリをソートします。
l_iが属するブロックを第一キーに、r_iを第二キーとして昇順にソートします。
l_iが属するブロックは\lfloor \frac{l_i}{b} \rfloorにより求められます。

ソート後(ブロック毎に区切ってあります。)
[1,3]
[2,5]
[0,8]

[4,4]
[3,5]
[5,7]

[7,7]
[7,8]

ソート後に順にクエリを処理します。
例えば、予め[2,5]の値は計算されていると仮定して[2,5] -> [0,8]を計算するときは、

  1. 右端を、[2,5] -> [2,6] -> [2,7] -> [2,8]という風に、+1ずつ区間を右に伸長しながら値を更新する
  2. 左端を、[2,5] -> [1,5] -> [0,5]という風に、-1ずつ区間を左に伸長しながら値を更新する

という感じです。

ABC243 参加記録

コンテスト中AC:A〜D

D - Moves on Binary Tree

公式解説の解法1とほぼ同じでした。

まず、根からXへの移動方法の文字列Tを得ます。具体的には、

Xが1となるまで以下を行う。

Tの末尾に、Xが偶数なら'L'を、奇数なら'R'を追加。
X\leftarrow \lfloor \frac{X}{2}\rfloorと更新。

上記が終了したら、Tを反転させる。

その後、i=1,2,...,Nについて、以下を行います。

S_i'U'なら、Tの末尾から文字を一つ取り除く。
そうでないなら、S_iTの末尾に追加。

これによって、Tは根から答えのノードまでの移動方法の文字列となります。あとはこれに従って、根(番号1)から計算していけば良いです。

計算量は、Xまでの文字列の大きさがlogXで、最終的なTの大きさもlog(10^{18})を超えないことから、O(N)だと思います。

提出コード

書いていて気づきましたが、最終的にT'U'を含まないので、答えの番号を求めるときの分岐は不要でした。

解法2の考え方も思いついたのですが、ビットシフトしなければいけないと思い断念しました。
単純に末尾に末尾への追加・削除だけで良いことを思いつけなかったです。