ツバサの備忘録

主に備忘録代わりに精進記録を載せていくつもりです。

AOJ 2443 Reverse Sort

問題
提出コード

解法

いろいろ嘘枝刈りBFSぽいことを考えましたが、全く解けませんでした…

AOJ 2443. ReverseSort - うさぎ小屋

↑こちらを参考にしました。
前から貪欲に揃えていくと、確実にN-1回以内で揃える事ができます。 操作回数が増えると、たどり着ける状態が指数的に増えていきます。
なので、前(後ろ)からだけ見ていくのでは、最終的な状態が増えすぎてしまいます。
そこで、前から半分だけ、後ろから半分だけを見て、どちらからも行けるような状態が存在するかどうか、を調べます。
ということで、N/2手だけ動かしてたどり着ける状態(と手数)を両方調べ、どちらにも存在する状態が無ければN-1、そうでなければそれぞれの手数の和の最小値を求めればよいです。

感想

半分全列挙の経験があまりなかったので、面白かったです。勉強になりました。

ABC166 E - This Message Will Self-Destruct in 5s

問題
提出コード

解法

もちろん、ペア(i,j)を全探索することは難しいです。
ペアとしてカウントされる条件を詳しく見ていきます。
i \lt jとしたとき、カウントされる条件は、

  • j - i = A_{i} + A_{j}

となります。
これを式変形すると...

  • j - A_{j} = A_{i} + i

となります。
jより小さくて、jとペアになれるiの個数は、上の条件を満たすiの個数になります。
あとは、jについて前から見ていき、A_{i} + iをそれぞれの値ごとにいくつ存在しているか、を見ればよいです。
やることは下の記事にある問題(と、そのページに貼られている類題)とほとんど同じです。

emtubasa.hateblo.jp

AOJ 2611 - 順位付け

問題
提出コード

解法

木の2乗DPとかなんとか言われてるやつです。
問題文をじっくり読むと、入力が木になっていて、そのなかで順位付けをしていくことがわかります。木全体の根は、入力時点で一番高さが高かった頂点です。
木の高さがすべて異なる場合は簡単に求めることができますが、同じ高さの場合があるのでさっと解くことはできません。
以下のようなDPを考えます。

  • dp[v][i] = vを根とする部分木について、高さの種類数がiとなるようにしたときの、高さの決め方

頂点が葉だった場合は、dp[v][1] = 1となります。
それ以外の場合、子についてそれぞれ探索してから、自分自身にマージしていくことを考えます。
tmp[i] = 子についていくつか見た後の、現時点での値(高さがi種類となるようなパターン数)
ここに、dp[c][j]達をマージしていきます。cは、今見ている子です。
tmp[i]dp[c][j]を組み合わせると、
新たな種類数は[max(i,j) , i + j]区間のいずれかの値になります。
新たな種類数がkになるパターンを考えます。
i,j種類を組み合わせてk種類にする際、

  • k種類の中から、i個を選び、tmp側の高さを全て割り振る

という操作をまず行うと、k種類の中から、すでに選択されたi個と、まだ選択されていないk-i個にわかれます。
ここから、j個の頂点に高さを割り当てなければなりませんが、この中で、k-i個は残りのj個のいずれかが選ばないといけないので無視します。
ということで、残ったi個の中から、j - (k - i)個を選び、jに割り当てる必要があります。
割り振る場所を決めてしまうと、順番は一意に決まるので、最終的に、i,j種類を組み合わせてk種類を作成する際のパターン数は、
_{k}C_{i} \times _{i}C_{(i + j - k)}通りです。
ということで、ここにtmp[i] \times dp[c][j]をかけたものを、新たに足しこめばよいです。
f:id:emtubasa:20200501143824j:plain
このようにして、tmpdp[c]を組み合わせて新たなtmpを作成、という操作を繰り返します。
その後、最終的なtmpについて、dp[v][i] = tmp[i - 1]とすればよいです。今見ている頂点v自身を種類数に加えるためです。
計算量についてですが、一見O(N^{4})かかるように見えますが、一番内側のループ以外が合わせてO(N^{2})になるみたいです。
詳しい解析はこちらを参照してください。

snuke.hatenablog.com

ABC164 D - Multiple of 2019

問題
提出コード

解法

ここでは、Sについて下からi桁目の数字をS_{i}と表記します。また、Sの文字列の長さ、すなわち|S|Nとします。
加えて、X2019で割った余りをX \ \%\  2019と表記します。
まず、Sについて、桁ごとに分解して表現してみると、
S_{1} \times 1 + S_{2} \times 10 + S_{3} \times 100 + ... S_{N} \times 10^{N-1}
となります。
S \% 2019は、次のように書くこともできます。
(S_{1} \times 1 \ \% \ 2019+ S_{2} \times 10  \ \% \ 2019+ ... S_{N} \times 10^{N-1}\  \% \ 2019)\  \% \ 2019
一般化すると、
 \displaystyle ( \sum_{i = 1}^{N} (S_{i} \times 10^{i-1} \ \% \  2019) ) \ \% \ 2019
ここで、Sum[k] = \displaystyle ( \sum_{i = 1}^{k} (S_{i} \times 10^{i-1} \ \% \  2019) ) \ \% \ 2019
と定義してあげると、
Sum[0] = 0,Sum[k] = (Sum[k-1] + (S_{k} \times  10^{k-1}) \ \% \ 2019) \ \% \ 2019

10^{k} \ \% 2019  = (10^{k-1} \ \% \ 2019) \times 10 \ \% \ 2019
の二つを組み合わせ、累積和のようにしてO(N)で計算することができます。
ここで、Si桁目からj桁目までを取り出した数(  i \leqq j )は、これをPとして、
 \displaystyle P =  \frac{\sum_{k = 1}^{j} (S_{k} \times 10^{k-1}) -  \sum_{k = 1}^{i-1} (S_{k} \times 10^{k-1})}{10^{i-1}}
Pが2019の倍数となるには、
P \ \% \ 2019 = 0
となっていればよいです。
2019と10^{X} は互いに素なので、先ほどの数式のうち分子のみに着目することで、結局
 (\sum_{k = 1}^{j} (S_{k} \times 10^{k-1}) -  \sum_{k = 1}^{i-1} (S_{k} \times 10^{k-1}) ) \ \% \ 2019 = 0
であれば、Pは2019の倍数ということがわかります。
これをさらに変形すると、
 (\sum_{k = 1}^{j} (S_{k} \times 10^{k-1} \ \% \ 2019)\ \% \ 2019 -  \sum_{k = 1}^{i-1} (S_{k} \times 10^{k-1}\ \% \ 2019)  \ \% \ 2019) = 0
となり、結局
 \sum_{k = 1}^{j} (S_{k} \times 10^{k-1} \ \% \ 2019)\ \% \ 2019 =  \sum_{k = 1}^{i-1} (S_{k} \times 10^{k-1}\ \% \ 2019)  \ \% \ 2019
という条件であるようなi,jのペアを探せばよいということがわかりました。
さて、この式は先ほど作成したSum[k]そのものの式で、
Sum[i] = Sum[j]
であるようなi,jのペアを探せばよいことになります。
Cnt[k] = Sum[x] = kとなるようなxの個数、という配列を用意しておき、

  1. Sum[i]を計算

  2. Cnt[Sum[i]]を答えに加算

  3. Cnt[Sum[i]]に1加算

という操作を、i = 0から繰り返して行けば、答えが求まります。Cntについては、取り得る添え字の値が[0,2019)なので、そのサイズだけ確保しておけばよいです。

おまけ

今回のように、SumCntを計算しながら答えを求めていく、というものはほかにもいくつかあります。
Cntは、今回添え字が最大で2019にしかならないのであらかじめ配列を確保しておくことができましたが、そうでない場合でも、列の長さがN以下のとき、種類も最大でN通りにしかならないことを利用すると、map等を駆使することで計算ができます。
Sumも、単純な累積和でいいもの等があります。
ほかにも、201910と互いに素でないケースが存在する問題等ありますので、是非見てみると良いと思います。
類題リスト↓

A - Zero-Sum Ranges

AGCの200点でかなりの難関といわれた問題です。しかし、今回の問題を知っていれば、おそらく解けると思います。今回のような操作をする問題の解法ツイート等では、この問題名がよくあがります。

E - Divisible Substring

コーナーケースに注意しましょう。

E - Rem of Sum is Num

少しトリッキーですが、帰着させることができます。

D - Candy Distribution

MIS.W新歓コンテスト2020

自分の大学にあるサークルが主催するコンテストに参加しました。
自分のコードはまとめてここに上げてあります。問題の前から順番にa,b,...と振ってあります。自分のコードを動かすことができるeの入力データも添えました。
長いのでかなり端折る部分があります。
コンテストリンク

Mistale

K = Nか、F=Nか、はたまたそのどちらでもないか、で場合分けをします。
サンプルに"Genocide"となるケースが存在しないので、

10 10 0 0

などと入力してチェックをしてあげたほうがよいでしょう。

Join MIS.W!

P,C,Mから、それぞれS_{i}で入力される人を引いていけば、最終的にすべてが0以下になっているかどうかで場合分けができます。

Okimochi Expression

Rの降順でソートし、得点が一番高い人について、"True"であれば(初期値が0の)カウントを+1,そうでなければ-1として、カウントが正か負かはたまた0になるか、で場合分けをします。

Random Card Battle

O(Nmax(A_{i})が間に合うので、全てのXについて愚直に試してしまえばよいです。 それぞれのカードについて独立なので、i番目に勝てる確率の最大値について計算し、その総和を求めると答えになります。
A_{i},B_{i}に対し、X_{i}を選択した場合、勝てる確率は
max(A_{i} + X_{i} - max(A_{i} - X_{i} , B_{i} + 1) + 1, 0)
です。出す可能性のある得点A_{i} + j区間として見ると見やすくなるかもしれません。

SABAKAN FEVER

面倒な入力の部分必要だったのでしょうか問題自体はbitDPを用いて解けます。
dp[S] = Sの集合について、問題の条件に違反していないかどうか
とします。
それぞれについて愚直に見るとO(2^{N}N^{2})かかってしまいますが、
とある集合Sについて、Sから一つの要素を取り出してiとした場合に、S\setminus iが問題の条件に違反してたら、その時点でSも問題の条件に違反してしまいます。そうでなければ、iが、S\setminus iの要素と隣接していないかを調べることで、
O(2^{N}N)に抑えることができます。
得点計算も愚直計算でそれぞれに対しO(N)でできるので、全体でO(2^{N}N)で解けました。
ちなみに、グーグル翻訳に投げてローマ字に直せばいいや、と思っていたのですが…
Google Translate

問題の指定ではsiなのに対し、翻訳ではshiになったり、北区がKitaNorthに表記ゆれしたり、台東区TaitoTaitungで表記ゆれしたり、他にもいろいろあったりで大変でした。

Jewelry Construction

二人の持っている宝石をp,q個とします。 よくよく考えると、p = qとなるのは、p = q = kの場合のみです。それ以外の場合(p \lt qとします)、最後にpをA面に置いたのは明らかで、その結果q = pX + yとなり、元々y個だったのがpX個増えてq個になったのだとわかります。
ここで、y \geqq pの場合、この場合も明らかに最後に宝石が増えたのはpをA面に置いた場合であり、yをA面に置いたターンは0倍であったことがわかります。
すると、0倍の操作については無視をして、q = pX + y (y \lt p)となるようなXの倍率で1回操作を行った、と考えてよいです。結局、y = q \% pになります。
これをお互い繰り返して行き、最終的に初期の条件、つまりp=0,q= Kになるかを調べればよいです。
この方法だと、初めに後手がK個、先手が0個持っているパターンが生じてしまい、K = 1,N =13,L=7のようなパターンで嘘解法になるように見えますが、この場合は、初手でX = 1としてあげることで、上手く調整ができるようになっています。
これらの操作をうまくまとめると、結局GCD(L,N-L) = KであればOKです。L + K \gt Nにのみ注意しましょう。

MIS.W scheduling?

実装が破滅しました(その1)。
曜日、時間は全て分になおします。
まずは、サークル活動に重複がないかを調べます。重複があり、この時点でN個すべてに参加できなければ"No"です。
次に、全体で参加できる数を調べます。N + M個の区間について区間スケジューリングで調べればよいです。
最後に、今調べた個数に、サークル全てに参加しながらたどり着けるかを調べます。
サークルとサークルの空き時間それぞれについて、尺取り法のようにしながら、講義を入れられるかどうか区間スケジューリングで試していきます。
サークルについて、0分開始、0分終了および、inf分開始、inf分終了という2つの番兵を入れてあげると楽になります。
f:id:emtubasa:20200423153052p:plain

Taberungo

食べるリンゴM個を決めたとき、それらを食べる順番は、明らかにBが大きい順です。
なので、Bでソートをします。
あとは、ナップサックDPのようにして解けばよく、

  • dp[i][j] = i番目までみて、j個のリンゴを食べた場合に得られる満足度の最大値、とします。

dp[i][j] = max(dp[i-1][j],dp[i-1][j-1] + A_{i} - B_{i}(j -1))
で更新すればよいです。

Contiguous Subsequences

実装が破滅しました(その2)。
頑張って二分探索をします。
総和がX以下となるような連続部分列の個数がP個以上あるかどうか、については、しゃくとり法を用いることでO(N)で調べることができます。
連続部分列全体の個数が奇数の場合は、P = N(N+1)/4 + 1として一度二分探索をすればよく、
偶数の場合は、P = N(N+1)/4P=N(N+1)/4 + 1の2パターンについて調べ、その平均を取ればよいです。

Dentaku

足し算によって区切られるので、掛け算について考えます。足し算も同様にすればよいです。
+で区切られる部分の連続する文字についての順列をまず考えます。
これはこんな感じで計算ができます。
次に、*を入れる箇所について考えてみると、文字がn個存在するときに、*n-1個あります。
これらを左から並べていくときに、文字の個数p*の個数qには、p  \geqq q + 1という関係がなりたっていなければなりません。
左端は必ず文字なので、これを除くと、p \geqq qを満たすように文字と*を並べる並べ方を求めることになり、これはカタラン数そのものです。ということで、ここを参考に計算すればよいです。
+パートについてですが、+で区切られる文字列を1つの大きな文字と見てあげて、*と同じことを考えればよいです。
文字列について、辞書順ソート等できちんと並び替えることに注意すれば、この問題に答えることができます。
後は、計算した値の積を求めればよいです。

感想

実装で破滅した問題がいくつかあったので悔しいです。
典型っぽい考え方を使いつつ、面白い部分もたくさんあったので楽しかったです。
自分の中で400点問題と5,600点問題の難易度が逆転していました。

AOJ 2828 - マトリョーシカ

問題
提出コード
解説ACです。マッチング、難しいですね…

解法

元の問題について、体積の最小値ではなく、見えているマトリョーシカの個数を求める問題は、
最小パス被覆問題そのものになっています。これは二部グラフの最大マッチングに帰着させることができます。
sourse、sinkと、マトリョーシカそれぞれに対してin,outの2つずつ頂点を用意してあげます。
あとは、sourseからn個のout、n個のinからsinkに加え、マトリョーシカijをしまうことができる場合に、マトリョーシカjのoutからiのinに向け、それぞれ容量1の辺を張ればよいです。
体積の最小値の場合は、これを応用して重み付き二部マッチングによって解くことができます。
容量は全てそのままで、jのoutからiのinに向けて張った辺について、コストをiの体積について符号を反転させた値とすることで、辺をカットする行為がjの体積だけ得をすることに結び付けられます。
その他の部分でコストがかかることはないため0にしてしまえばよいです。
辺について一番異なるのは、最小費用流の場合、容量をn流すと指定しなければならない部分で、最終的に一番外側になるマトリョーシカ分もフローを流さなければなりません。これは、それぞれのマトリョーシカのoutから、直接sinkにコスト0、容量1の辺を張ることで表現できます。
あとは、nだけ流すのにかかるコストの最小を求め、その-1倍したものを、体積の合計から引くことで、最終的に一番外側になっているマトリョーシカの体積が求まります。
自分は、-1倍して解かずに、これを用いて解きました。2つのマトリョーシカを結ぶ辺については、m-(体積)、outからsinkに向けてはmのコストに直します。すると、
mn - (フローを流して求まった値)が最終的に得をする分の体積になります。
f:id:emtubasa:20200422233042p:plain

AOJ 2703 - サイコロスタンプ

問題
提出コード

解法

あらかじめ、i番目のサイコロが転がった結果記される数字と座標のペアリストを作成しておきます。

  • dp[S] = サイコロの集合Sについて、これらを最適な順番で転がした際に得ることができる得点の最大値

とすると、答えはmax(dp[S])です。
dp[S]の求め方について考えます。
Sの中で、i番目のサイコロを最後に転がした、ということを考えてみます。
このとき、S \setminus\{i\}と、i番目のサイコロが通る座標で重複する部分があれば、dp[S \setminus \{i\}]から差し引かなければなりません。
ところが、重複する座標に今どの数字が書かれているか、というのはこれらの情報からは読み取ることができません。
ということで次に逆順、つまり、iを最初に転がしたとき、について考えてみます。
このとき、S \setminus\{i\}と、i番目のサイコロが通る座標が重複する部分について考えてみると、その座標に何がかかれていようが、i番目のサイコロがその座標に書き込む数字は別のサイコロによって上書きされてしまいます。
ということで、Sの中で、最初にiを転がした際に得られる得点の最大値は、
dp[S \setminus\{i\}] + \sum (i番目のサイコロがS\setminus\{i\}と重複せずに書き込む座標の値)
となります。
dp[S]は、上記の計算をi \in Sについて計算したうちの最大値となります。
O(N max(|rot|)2^{N})です。自分は座標管理にmapを用いたので、\logが付きます。