こんな問題が出てきたらあなたはどうするだろうか?

引用元:長い長い数列

数列 A = (A_1, A_2, …, A_n) と数列 B = (B_1, B_2, …, B_m) が与えられる。
n 行 m 列のマス目に | A_i – B_j | を書き込む。これらを小さい順に並べたとき k 番目にくる値を求めよ。
ただしnとmの取り得る最大値は30,000である。

このような問題です。nとmは最大で30,000ということはマスは最大900,000,000個存在することになります。一応intの範囲内なのが唯一の救いです。

数列が短ければどうやってでも解けるのだが

どうやって考えればよいでしょうか? まずは普通の考え方。

実際に各マスに入る数をひとつずつ計算していきます。

絶対値がついているため単なる引き算と比べて時間がかかります。引き算であれば2800ms(たくさん計算すると結構時間がかかります)ですが、Math.Abs(ns[x] – ms[y])をすべて計算しようとすると6600msかかります。ソートしたいのでこれらをリストに格納してソートします。すると全部で96000msかかりました。1分以上かかるというのです。

リストと配列の差 要素の数が増えるとその差は無視できない

鳩でもわかるC#管理人には配列よりもリストを使いたがる悪いクセがあります。

そこで最初から配列に格納してしまえばリストに格納していくより処理速度は速くなります。計算が終わってループを抜けるまでにかかった時間は9700ms。リストに格納し終えたときの時間は13600msだったのでかなり改善されています。

しかしそのあとソートの処理が必要です。この部分は同じなのでやはり1分以上かかってしまいます。

いかにして計算量を減らすか?

この問題はランクアップ問題集(ユーザー同士で解答を教え合いコードを公開することが許されている)の二分探索カテゴリ内のものであり、二分探索をうまくつかって処理時間5秒以内に答えを導き出すというものです。

解説には以下のようなことが書かれています。

数列の k 番目に小さい値は、「数列に x 以下の値が k 個以上含まれるような値 x の最小値」と言い換えることができる。言い換えたあとの問題には、数列に x 以下の値が k 個以上含まれているならば、x ≦ y を満たすすべての y について、数列に y 以下の値が k 個以上含まれているという単調性がある。だから二分探索によりこの境界を求めれば良い。

とのことです。

改善されたコード

上記コード内で使われているLowerBoundメソッドとUpperBoundメソッドを示します。

これだと約200msで結果が出力されます。