FHC 2017 R2 C - Fighting all the Zombies
問題
Fighting all the Zombies | Facebook Hacker Cup 2017 Round 2
問題概要
RPGをやっている。主人公は魔法使いである。
体のゾンビがいる洞窟がある。番目のゾンビの強さはである。いま、レベリングのために、この洞窟を回周回しようと考えている。この時、主人公は洞窟に入るたびに体全てのゾンビを倒して洞窟から出てきて、再び洞窟に入ると体全てのゾンビが復活している。
主人公は、本の杖を持っている。番目の杖の強さはである。それぞれの杖は洞窟に入るごとに一度しか使えず、杖を使うと一体のゾンビを倒すことが出来る。
はじめ、主人公は種類の呪文を覚えている。番目の呪文は強さを持っており、それは番目の杖を使うことで発動できる。
さて、主人公は回この洞窟にいくことになるが、回目に洞窟に入る前に新たに個の呪文を覚える。その個の呪文の強さは全てであり、全て強さの杖によって発動可能な呪文である。ただし、呪文の強さと杖の強さに大きな差はなく、具体的にはを満たす。また、全ての呪文は区別して考える。
この状況で、回目に洞窟にいった時の体のゾンビの倒し方が何通りあるか知りたい。この時、少なくとも1体のゾンビに対して別の呪文を使うような組み合わせは区別して数える。
回目に洞窟に行った時のゾンビの倒し方の組み合わせが通りあるとしたときのをで割った余りを答えよ。
アイデア
設定が複雑で分かりにくい。1つずつまとめていく。
まず、番目のゾンビを倒すためには強さの呪文しか使えないということなので、初期状態では組み合わせは1通りしかない。また、杖に注目すると、新しい呪文を覚えていったとしても、番目の杖で発動可能な呪文の強さはかかの3種類に限られるということが分かる。
さて、強さ1の杖から順番に、どの強さの呪文を発動するかを考えていく。1の杖では、(使える呪文が1つ以上あるのなら)強さ1か2の呪文を使うことが出来る。そして、2,3,4,… と弱い方から順に呪文の強さを選んでいき、次に強さの杖でどの呪文を発動するかということを考えると、この状況で強さが埋まっていることは有り得ないが、とが埋まっていることはあり得る。このことから次のようなDPを考える。
dp[i][prev][now] = i番目の杖に注目していて、強さi-1の呪文は既に使われたか(prev)、強さiの呪文は既に使われたか(now)という状況の時の組み合わせの個数
初期状態としてはdp[1][0][1]=1ということになり、最終的には求めたいものはdp[N+1][1][0]となる。ただ、このDPの計算を回もやろうとするととなって間に合わない。ただ、遷移に注目してみると周回している間、その1回1回の間で遷移が変化するのは1箇所だけなので、その無駄をなくしてなんとかしたいと考えられる。
ここで、DPのiとi+1の間の遷移がどうなっているのかを図で書いてみるとこのようになる(はじめはdp[0][0][1]=1から始まっているので、意味のある遷移だけを書いた)。
この図を見て分かる通り、dpとして式を定義したはいいものの、結局dp[*][0][1]とdp[*][1][0]しか必要ないことに気づく。ということで、前者を,後者をと置く。そして、杖がのそれぞれの強さの呪文をいくつずつ使えるのかをとすると、次のような漸化式が成り立つ。
これらは行列の形に直せて、という初期値も合わせると、個の行列の積によって途中の遷移が表されることになり、答えはということになる。
この形になったところで、回の周回時に毎回この行列積を初めから計算していたのでは間に合わないので、更新されたところだけを計算し直すという方法を取る。
コンテスト本番時は平方分割によってでの実現を図ったが、思った以上にケースごとの処理時間が長く、時間内に提出できなかった。そこで、平方分割ではなく、SegTreeによってこの更新を行っていき、答えを求める。それによって、1回ごとにでの更新が可能になる。さすがにが大きくなるとこの2つにも差が出るのだなあという感じがする。
平方分割よりかなり速くなったけど4分くらい入力に対してかかった…まあギリギリか。ただ意外と行列をSegtreeにのせる実装が最大値のSegtreeのテンプレがあったらちょっと書き換えるだけで出来て、意外と大変じゃないのかという感想。
実装(C++)
続きを読むCF 749 E - Inversions After Shuffle
問題
問題概要
からまでの数を並べた長さの順列がある。1回だけ以下の操作を行う:
全ての区間個のうち、区間(から)をランダムに選ぶ。そして、その区間の長さをとし、その区間に対して、シャッフルを行う(つまり、通りの並び替え方のうち、ランダムに1つ選ぶ)。
この操作の後にできあがる数列の反点数の期待値を求めよ。
アイデア
以下、配列は問題に従って1-indexで考えることにする。
考えるべき区間の選び方は個あるわけなので、これが分母になってくるとして考えていくことにする。
考察にあたり、もう一つ重要なことはただ単に順列の全体をシャッフルした時の反転数はということである。これは、1つの配列に対して、それをreverseしたような配列を考えてみると、その2つの配列の反点数の和がになっており、そのようなreverseしたもののpairが全ての順列に対して見つかるので、それを2で割ったものということである。
上記の考えから、この問題において求めるべき値は与えられる順列の区間 ]の反点数をとおくと、次のようになる:
分子がなぜこのような形になるのか、区間 ]をシャッフルした後に、その順列全体の反点数がどのように変化するかを考える。すると、2つのindexに対して、反点数に変化があるかどうかは、どちらかのindexがこのシャッフルの区間の外ならその順番が入れ替わることはないので反点数は変化しないわけである。つまり、反点数が変化しうるのはこの区間内についてのみ注目すればいいということになる。そして、前述の通り長さの順列をシャッフルした時の反点数の期待値はであるから、その元の反点数を引いてその期待値を足しているという式の形になっている。
さて、ただこれを愚直に計算するのでは間に合わない。分母はなので、分子をどうするか考えてみると、全体の反点数についてはBITなどをつかうことによってで計算が可能であり、この分数についても、に着目すれば種類の値がそれぞれ何回ずつ現れるのかが簡単にわかるのでで計算できる。
そして残るはの計算である。以下で、これについて考える。
いま、をまとめて計算することを考えてみる。そのために、をからの方向に動かしながら考えていく。そして、配列を考えて、とする。
さて、このの値について計算したいが、この値はを使って表すことが出来る。それは区間 ]を ]と ]に分けて考えることが出来るからである。
そして、いまは始点を固定しているので、この区間に対して例えば番目の値は1つの区間( ])に入り得る。一方で、番目の値に注目すると2つの区間( ]と ])に入る。つまり、この2つの区間でこの反転がカウントされる。このようにを固定したときに番目の値は全部で個の区間に入ってくることになる。
つまり、を計算するときに、なるを考えて、その中でとなっている位置のみに対してこの上記の反点数カウントをすればよいことになる。これは、まさにBITによって実現される。
(自分の用意してたBITにちょっとバグがあってそれに気づいて直すまでに時間かかった...このBITで何問か通してたはずなんだけど、見つかってよかった)