裏紙

ほぼ競プロ、たまに日記

CS Academy #42 - Xor Submatrix

問題

CS Academy

問題概要

長さnの配列vと長さmの配列uが与えられる。これらから、n * mの行列Aを作る。A_{ij} = v_i \ XOR \ u_jで行列の各項を与える。 この中で部分行列を取り出し、その部分行列の要素の全てのXORを取る時に、得られる値の最大値を求めよ。

  •  1 \le n,m \le 1000
  •  0 \le v_i , u_i \lt 2^{29}

イデア

部分行列の左上隅のindexを(ly,lx)、右下隅のindexを(ry,rx)と表すことにしよう。ここで、部分行列内の全てのXORを取るということがどういうことになるのかを考えてみると、各v_i (ly \le i \le ry)rx-lx+1回だけ全てのXORの計算に現れてくることになる。つまり、このrx-lx+1の偶奇によって、各v_i (ly \le i \le ry)が総和に現れるか否かが決まる事がわかる。

この考察より、以下の3パターンに分けて考えよう:

  1. 縦も横も偶数幅
  2. 縦か横のどちらか一方が偶数幅でもう片方は奇数幅
  3. 縦も横も偶数幅

1の場合については、全ての項が偶数回現れることになるので0になる。2の場合については、奇数幅を構成する側は偶数回現れることになるので打ち消し合い、偶数幅を構成する側のみのXORを取ることになる。3の場合は両方の幅を構成するもののXORを取れば良いことになる。

そうすると、部分行列の(ly,lx)(ry,rx)を決めればvuについてXORでの累積和を持つことによってこの部分行列内のXOR和をO(1)で得ることができる。

よって、部分行列内を全て計算する必要はなく、各配列v,uでそれぞれこのような累積和を取る話に問題は変わってくる。そこで、この問題を言い換えると、

vから奇数幅で得られるXOR和の値を集めた集合Vと、uから奇数幅で得られるXOR和の値を集めた集合Uから1つずつ値を選んでXORを取って最大値を構成するという問題になる。これはTrieを使うことで求められる。具体的には、Vの要素を全てバイナリとして表したものをTrie上にのせ、U内の各要素に対して大きいビットの方から貪欲にそのビットを1に出来るならその方向に進むということをすればよい。

実装(C++)

続きを読む

CS Academy #44 - Array Elimination

問題

CS Academy

問題概要

素数nの配列aが与えられる。いま、配列のindexを1つ指しているポインタpがあり(はじめはp=0)、そのポインタに対して以下の3つの操作ができる:

  • pを1増やす
  • pを1減らす
  • pが指している位置の要素を削除し、pは1つ前の要素か1つ後の要素を指すようにどちらかを選ぶ

この3番目で行う削除の順番を、non-decreasing orderで行うために必要な操作の回数の最小値を求めよ。

  •  1 \le n \le 10^5
  •  1 \le a_i \le 10^9

イデア

まず、配列aの中に同じ要素がなければ、削除の順番は一意に決定するので操作は貪欲にしていけば良いことになる。この操作の一つ一つを本当にシミュレーションするとなると回数はO(n^2)回かかるので当然間に合わない(ただ、次に行くべき位置が決まってるので、移動は省けそうに見える)。

もし、aも中に同じ要素がある場合は、要素の値でグループ分けをして削除の処理をしていくことになる。この時、グループ間の削除の順番は入れ替えることが出来ないが、グループ内であれば削除の順番を入れ替えても良いことになる。

今、x未満の値の削除が終了し、xの値のグループを削除していくという状況を考えよう。さて、上の3つの操作を見比べてみると、pが値xの位置を指しているときにするべきことは3番目の操作しかありえないということが分かる(1番目の操作や2番目の操作もその中に含んでいるし、その後の状況が良い)。ということを考えれば、このグループ内で最後に削除されることになるのは配列の左端のxまたは右端のxのどちらかということになる。

よって、次のようなDPが考えられる:

dp[x][0/1] = 値x以下は全て削除完了し、値xのグループに関しては最後に削除したのが左端(0)/右端(1)のときの(削除を伴わない移動)操作回数の最小値

xの部分については座圧することで、n種類になる。小さい方からi番目の値であると考えることにすると、このdpを計算するのに必要なのはdp[i-1][0/1]ということになる。

dp[i][0]を計算したいと思った場合には、dp[i-1][0]とdp[i-1][1]のそれぞれからの遷移を考えるわけだが、どちらについてもやるべき最適な動作は共通で、値xの右端まで(値を削除しながら)ポインタを進めていき、折り返して左端まで行くという操作になる。その間で必要になる操作の回数はその区間にあるxより大きい値の個数に一致することになる。これは、dpを進めていくと同時にBITを更新することでその個数を数えることができ、全体としてはO(n log n)で実現出来る。

実装(C++)

続きを読む