読者です 読者をやめる 読者になる 読者になる

裏紙

ほぼ競プロ、たまに日記

ARC 068 E - Snuke Line

問題

E: Snuke Line - AtCoder Regular Contest 068 | AtCoder

問題概要

M+1個の駅があり、駅に対して0からMまでの番号が振られている。列車は駅0からd駅ごとに停車する。また、N種類の名産品があり、i番目の名産品は駅l_iから駅r_i区間で売られており、その駅に停まると名産品が買える。d=1,2,3, \ldots ,Mの時に買える名産品の個数をそれぞれ求めよ。

  • 1 \le N \le 300000
  • 1 \le M \le 100000
  • 1 \le l_i \le r_i \le M

イデア

まず間隔dを固定して考えてみる。すると、区間の幅(r_i - l_i +1)がdよりも大きい時は、その区間のどこかしらに必ず訪れることになるので、その名産品を買える。一方で、幅がd以下の時はたかだか1回しか訪れることができないのが分かる。

ここで、幅がdより大きい区間については必ず買えるので無視して、幅d以下の区間についてのみ考えると、この区間についてはたかだか1回しか訪れないことがわかっているから区間の累積和を使ってはじめにその位置に含まれる区間の個数を計算してから、dの倍数の位置を調べていけばよいことになる。これでO(N+M)で実現できる。

問題は、d1からMまで動くので、このままでは間に合わない。ただ、幅d以下の区間dを増加させるごとにその数は単調増加になっているので、1からはじめて、上で行った処理をBIT上で行うことで高速に処理することが出来る。

実装(C++)

続きを読む

FHC 2017 R2 C - Fighting all the Zombies

問題

Fighting all the Zombies | Facebook Hacker Cup 2017 Round 2

問題概要

RPGをやっている。主人公は魔法使いである。

N体のゾンビがいる洞窟がある。i番目のゾンビの強さはiである。いま、レベリングのために、この洞窟をM回周回しようと考えている。この時、主人公は洞窟に入るたびにN体全てのゾンビを倒して洞窟から出てきて、再び洞窟に入るとN体全てのゾンビが復活している。

主人公は、N本の杖を持っている。i番目の杖の強さはiである。それぞれの杖は洞窟に入るごとに一度しか使えず、杖を使うと一体のゾンビを倒すことが出来る。

はじめ、主人公はN種類の呪文を覚えている。i番目の呪文は強さiを持っており、それはi番目の杖を使うことで発動できる。

さて、主人公はM回この洞窟にいくことになるが、i回目に洞窟に入る前に新たにS_i個の呪文を覚える。そのS_i個の呪文の強さは全てW_iであり、全て強さZ_iの杖によって発動可能な呪文である。ただし、呪文の強さと杖の強さに大きな差はなく、具体的には | W_i - Z_i | \le 1を満たす。また、全ての呪文は区別して考える。

この状況で、i回目に洞窟にいった時のN体のゾンビの倒し方が何通りあるか知りたい。この時、少なくとも1体のゾンビに対して別の呪文を使うような組み合わせは区別して数える。

i回目に洞窟に行った時のゾンビの倒し方の組み合わせがP_i通りあるとしたときの\displaystyle \sum_{i=1}^{M} P_i10^9 + 7で割った余りを答えよ。

  •  1 \le N, M \le 800000
  •  1 \le W_i , Z_i \le N
  •  1 \le S_i \le 10^9

イデア

設定が複雑で分かりにくい。1つずつまとめていく。

まず、I番目のゾンビを倒すためには強さiの呪文しか使えないということなので、初期状態では組み合わせは1通りしかない。また、杖に注目すると、新しい呪文を覚えていったとしても、i番目の杖で発動可能な呪文の強さはi-1ii+1の3種類に限られるということが分かる。

さて、強さ1の杖から順番に、どの強さの呪文を発動するかを考えていく。1の杖では、(使える呪文が1つ以上あるのなら)強さ1か2の呪文を使うことが出来る。そして、2,3,4,… と弱い方から順に呪文の強さを選んでいき、次に強さiの杖でどの呪文を発動するかということを考えると、この状況で強さi+1が埋まっていることは有り得ないが、ii+1が埋まっていることはあり得る。このことから次のようなDPを考える。

dp[i][prev][now] = i番目の杖に注目していて、強さi-1の呪文は既に使われたか(prev)、強さiの呪文は既に使われたか(now)という状況の時の組み合わせの個数

初期状態としてはdp[1][0][1]=1ということになり、最終的には求めたいものはdp[N+1][1][0]となる。ただ、このDPの計算をM回もやろうとするとO(NM)となって間に合わない。ただ、遷移に注目してみると周回している間、その1回1回の間で遷移が変化するのは1箇所だけなので、その無駄をなくしてなんとかしたいと考えられる。

ここで、DPのiとi+1の間の遷移がどうなっているのかを図で書いてみるとこのようになる(はじめはdp[0][0][1]=1から始まっているので、意味のある遷移だけを書いた)。

f:id:imulan:20170202002346p:plain

この図を見て分かる通り、dpとして式を定義したはいいものの、結局dp[*][0][1]とdp[*][1][0]しか必要ないことに気づく。ということで、前者をa_i,後者をb_iと置く。そして、杖ii-1, i, i+1のそれぞれの強さの呪文をいくつずつ使えるのかをC_{i,0} , C_{i,1}, C_{i,2}とすると、次のような漸化式が成り立つ。

a_{i+1} = b_i * C_{i,2}

b_{i+1} = a_i * C_{i,0} + b_i * C_{i,1}

これらは行列の形に直せて、a_1 = 1, b_1 = 0という初期値も合わせると、N個の行列の積によって途中の遷移が表されることになり、答えはb_{N+1}ということになる。

この形になったところで、M回の周回時に毎回この行列積を初めから計算していたのでは間に合わないので、更新されたところだけを計算し直すという方法を取る。

コンテスト本番時は平方分割によってO(\sqrt{N})での実現を図ったが、思った以上にケースごとの処理時間が長く、時間内に提出できなかった。そこで、平方分割ではなく、SegTreeによってこの更新を行っていき、答えを求める。それによって、1回ごとにO(logN)での更新が可能になる。さすがにNが大きくなるとこの2つにも差が出るのだなあという感じがする。

平方分割よりかなり速くなったけど4分くらい入力に対してかかった…まあギリギリか。ただ意外と行列をSegtreeにのせる実装が最大値のSegtreeのテンプレがあったらちょっと書き換えるだけで出来て、意外と大変じゃないのかという感想。

実装(C++)

続きを読む

CF 749 E - Inversions After Shuffle

問題

Problem - E - Codeforces

問題概要

1からnまでの数を並べた長さnの順列がある。1回だけ以下の操作を行う:

全ての区間\frac{n(n+1)}{2}個のうち、区間(lからr)をランダムに選ぶ。そして、その区間の長さをk(=r-l+1)とし、その区間に対して、シャッフルを行う(つまり、k!通りの並び替え方のうち、ランダムに1つ選ぶ)。

この操作の後にできあがる数列の反点数の期待値を求めよ。

  • 1 \le n \le 100000

イデア

以下、配列は問題に従って1-indexで考えることにする。

考えるべき区間の選び方は \frac{n(n+1)}{2}個あるわけなので、これが分母になってくるとして考えていくことにする。

考察にあたり、もう一つ重要なことはただ単に順列の全体をシャッフルした時の反転数は\frac{n(n-1)}{4}ということである。これは、1つの配列に対して、それをreverseしたような配列を考えてみると、その2つの配列の反点数の和が\frac{n(n-1)}{2}になっており、そのようなreverseしたもののpairが全ての順列に対して見つかるので、それを2で割ったものということである。

上記の考えから、この問題において求めるべき値は与えられる順列a区間 [ l, r ]の反点数をinv(l,r)とおくと、次のようになる:

 \displaystyle \sum_{ 1 \le l \le r \le n} \frac{ inv(1,n) - inv(l,r) + \frac{(r-l+1)(r-l)}{4} }{ \frac{n(n+1)}{2} }

分子がなぜこのような形になるのか、区間 [ l, r ]をシャッフルした後に、その順列全体の反点数がどのように変化するかを考える。すると、2つのindexに対して、反点数に変化があるかどうかは、どちらかのindexがこのシャッフルの区間の外ならその順番が入れ替わることはないので反点数は変化しないわけである。つまり、反点数が変化しうるのはこの区間内についてのみ注目すればいいということになる。そして、前述の通り長さnの順列をシャッフルした時の反点数の期待値は\frac{n(n-1)}{4}であるから、その元の反点数を引いてその期待値を足しているという式の形になっている。

さて、ただこれを愚直に計算するのでは間に合わない。分母はconstなので、分子をどうするか考えてみると、全体の反点数についてはBITなどをつかうことによってO(nlogn)で計算が可能であり、この分数についても、r-lに着目すればn種類の値がそれぞれ何回ずつ現れるのかが簡単にわかるのでO(n)で計算できる。

そして残るは \sum_{ 1 \le l \le r \le n} inv(l,r)の計算である。以下で、これについて考える。

いま、inv(l,*)をまとめて計算することを考えてみる。そのために、lnから1の方向に動かしながら考えていく。そして、配列bを考えて、b_i = \sum_{i \le j \le n} inv(i,j)とする。

さて、このb_iの値について計算したいが、この値はb_{i+1}を使って表すことが出来る。それは区間 [i , j ]を [i , i ]と [i+1 , j ]に分けて考えることが出来るからである。

そして、いまは始点を固定しているので、この区間に対して例えばn番目の値は1つの区間( [i , n ])に入り得る。一方で、n-1番目の値に注目すると2つの区間( [i , n ]と [i , n-1 ])に入る。つまり、この2つの区間でこの反転がカウントされる。このようにiを固定したときにj番目の値は全部でn-j+1個の区間に入ってくることになる。

つまり、b_iを計算するときに、i \lt jなるjを考えて、その中でa_i \gt a_jとなっている位置のみに対してこの上記の反点数カウントをすればよいことになる。これは、まさにBITによって実現される。

(自分の用意してたBITにちょっとバグがあってそれに気づいて直すまでに時間かかった...このBITで何問か通してたはずなんだけど、見つかってよかった)

実装(C++)

続きを読む