TCO13 Round 1C
TCO13 Round 1C
http://community.topcoder.com/stat?c=round_overview&er=5&rd=15585
250に16分。500に46分。チャレンジに1回失敗。511位でTCO13 Round1を通過。Room 34。レートは1323から1347に上がった。
Problem | Status | Points | |
---|---|---|---|
250 | TheArray | System Test Passed | 170.76 |
500 | TheOlympiadInInformatics | System Test Passed | 382.96 |
1000 | TheKnights | Unopened |
TheArray
http://community.topcoder.com/stat?c=problem_statement&pm=12455&rd=15585
16分。170.76点。
- 問題を読み終わってすぐに全探索の問題であると考えた
- 与えられた情報から取りうる最大の要素の値を答える問題。与えられる情報は要素の数、要素間が取りうる最大の差、初めと最後の要素の値
- ここから頭の中に何となく浮かんでいたのは以下のような図であった(不思議なことにしっかり作図すると印象が違う)
- 左右から要素間が取りうる最大の差を用いて要素を置いて行く。左右からの要素が中央で出会ったときの差が高々dであれば高いほうの要素は解の候補となる
- 左右からの要素が中央で出会ったときの差がdより大きい場合は条件に合わない。解の候補にもならない
- これを素直に実装した
- 反復の回数と右からの距離の算出方法に手間取り、16分もかかってしまった
class TheArray { public: int find(int n, int d, int first, int last) { int ip = std::max(first, last); for (int i = 0; i < n - 1; i ++) { int f = first + i * d; int l = last + (n - 2 - i) * d; if (std::abs(f - l) <= d) ip = std::max(std::max(f, l), ip); } return ip; }; };
- 制約違反しているテストケースでチャレンジを行おうとしたが、キャンセルされた
- 最後の制約を見逃していたのが原因であった
|first - last| will be at most (n-1)*d.
- そもそもこの制約がなければ自分の解法は成立しないか...?
- 自分の実装で試していないテストケースを使ってチャレンジし、失敗した
- 自分の実装でも同じ答えを返した
- 写経しているのにもったいない...
- 今回のチャレンジの教訓
- 焦ってチャレンジしてもいいことはない
- チャレンジするテストケースは自分の実装で試したものを用いる
TheOlympiadInInformatics
http://community.topcoder.com/stat?c=problem_statement&pm=12456&rd=15585
46分。382.96点。
- 読み終わってすぐ、二分探索の問題であることに気付く
- しかも整数の二分探索だ。過去にばっちり考察したぞ...(Suminator - agwの日記 - TopCoder部)
- アルゴリズムCでの実装例は以下であった
int binarysearch(int v) { int l = 1; int r = N; int x; while (r >= l) { x = (l+r)/2; if (v < a[x].key) r = x-1; else l = x+1; if (v == a[x].key) return a[x].info; } return -1; }
- 当時以下のような作図もしたし、間違いようがない...(図は再掲)
- 意気揚々と実装を開始
- ...
- ...
- ...いつ反復を終了すればよいんだろう?
- 後で気付いたがアルゴリズムCの例は「単調非減少な数列a1, ..., aNからai = kとなるようなiを求める探索」であるのに対し、今回の問題は「単調減少な数列a1, ..., aNからai ≦ kとなるような最小のiを求める探索」を実装する問題であった
- さて問題を整理しよう。自分より点数の高い人が高々k人いる最小の点数を探したい
- 自分がm点取ったときにm + 1点以上の点数を取っている人が高々k人であればmは有効
- 有効な最小のmを探す
- 初めの例で考えてみる。4つの部屋それぞれの得点の合計が4点、7点、0点、5点
- 自分より点を取っている人の数が高々0人、つまりいなくなってしまう最小の得点を求めたい
- 自分が0点取るとする。1点以上取っている人は最大で16人。これは条件である0人を超えてしまっている
- 自分が1点取るとする。2点以上取っている人は最大で7人。これも条件である0人を超えてしまう
- これを繰り返す。自分がi点取ったときにi + 1点以上取っている人の数をaiとすると、aiは以下のような数列となる
- 自分より点を取っている人の数がいないのは自分が7点以上取っている場合であり、最小の得点は7点だ
- 個人が取りうる点数の範囲は[0, 109]である
- ここで記載した方法単純な反復だと計算量はO(MN)。収まりきらない
- しかし、単調減少な数列であるので二分探索を用いればO(MlogN)に抑えることが出来そうだ
- 少し考えるとこの問題で求められているのは有効な区間がどこから始まっているかであることが分かる
- うーむ、二分探索をどのように実装してよいかさっぱり想像が付かない
- 蟻本の二部探索の章を見てみる
- あ
- 載ってた
- また、反復を条件は以下だ
- 図示してみよう
- 「単調非減少な数列a0, ..., an-1からai ≧ 3となる最小のiを探す」問題を蟻本の手法で実装し、l、uと中央値mの動きを観察する
- mは(l + u) / 2として算出する
- 数列は「2、2、3、3、5」としてみよう
- nを数列の要素数とする。この場合nは5である
- また、数列はa0, ..., an-1であることに注意しよう
- をー、こうなるのか
- 数列の要素をずらして並べてみたくなる
- やってみた
- 一貫しているなぁ...
- 区間には必ず有効な要素が含まれていることにも注目したい
- amが区間に含まれている場合(amが有効な場合)
- 次の区間、(l, m]には有効な要素が *必ず* 含まれている(少なくともam)
- amが区間に含まれていない場合(amが無効な場合)
- 次の区間の(m, u]に有効な要素が含まれている(かもしれない)
- 少し微妙な言い回しにしたのは、数列が目的の要素を含まない場合を考慮したからだ
- 例えば「2、2、2、2、2」の数列ではai ≧ 3となる最小のiを求めることが出来ない。元々3以上の値が含まれていないからだ
- 同様に図示してみる。この図は初期値として与える区間を誤った失敗例であることに注意
- 数列に3が含まれていないのに関わらず、最後の要素(2)を示している
- これはよくない
- 探索が終了したときの区間が(n - 1, n]である、すなわちuがnであれば探索に失敗したことを表す
- これはすごい
- 右閉半開区間を伴った二部探索にはさらに利点がある
- 数列の要素数がnであると考える
- [0, n-1]以外の添字でアクセスするとセグメント違反するものとしよう
- つまりmが-1やnであるとき、amへのアクセスはプログラムを停止させてしまう
- だが、右閉半開区間を伴った二部探索ではmが必ず[0, n-1]に収まるため気にしなくてよい
- これは本当にすごいことだと思う
- ここまでの考察を元に、再度実装してみた
以下はシステムテストを通る実装。
class TheOlympiadInInformatics { public: int find(std::vector<int> sums, int k) { int l = -1, u = 1000 * 1000 * 1000; while (u - l > 1) { int m = (l + u) / 2; long long s = 0; EACH (it, sums) s += *it / (m + 1); if (s > k) { l = m; } else { u = m; } } return u; }; };
- すっきりした
おまけ
- 閉空間と左閉半開区間の挙動を図示する
- 左閉半開区間
- m = u - 1のとき、m + 1 = uとなり無限ループを引き起こす
- 図から見てもかなり不自然だ...
追記(2014/1/14)
int lower_bound(const std::vector<int>& a, int x) { int l = -1, u = a.size(); while (u - l > 1) { int m = (l + u) / 2; (a[m] >= x ? u : l) = m; } return u; }
- しかし、binary_searchのように「値があるかどうかを探索する」ために右閉半開区間を用いると冗長な実装となってしまう
bool binary_search(const std::vector<int>& a, int x) { int l = -1, u = a.size(); while (u - l > 1) { int m = (l + u) / 2; if (a[m] == x) return true; // 発見! (a[m] > x ? u : l) = m; } return a[u] == x; // この部分が冗長(未検査の値が一つ残っている) }
- この場合は閉区間を用いた探索を行ったほうが実装がすっきりする
bool binary_search(const std::vector<int>& a, int x) { int l = 0, u = a.size() - 1; while (u - l >= 0) { int m = (l + u) / 2; if (a[m] == x) return true; if (a[m] > x) { u = m - 1; } else { l = m + 1; } } return false; }
- 最後に、実装のコツも記載しておく
- lower_bound
- 右閉半開区間を用いる
- 右側に冗長な要素を用意しておく
- 対象が0-indexで要素nの配列の場合は(-1, n]とする
- 要素が一つに絞り込まれるまで探索をする
- 反復条件はu - l > 1(「要素が2つ以上あれば」)