ご存じでない方はひとまずこちらを参照。
アジア地区予選はこちらを参照。
三度目の参加になる。
初年度。二回生のとき。17位。残念ながら学内4位だったので予選通過ならず。
二年目。三回生のとき。10位。アジア予選進出。本選では13位ぐらい。
そして今年である。
combatというチーム名で出場した。 高校のときに出場した東工大のSuperConというコンテストの時にも使っていた。 そのときに一緒だったK氏、さらに当時同じクラブであったM氏とで編成したチームである。 同じ大学なんだよなぁ。 役割は特に決めてなかったけど、私がメインプログラマ、K氏がサブプログラマ、 M氏がXPのサポート要因、みたいな感じに。 結果は5位で予選通過(?)。 確実に去年よりも技量も上がったし、それなりの手ごたえもあった。 去年より順位も上がったけど、ただなんというか、色々惜しかった。
今年は去年よりも色々変わった。変わりすぎて動揺しまくって、 プレッシャーのあまりここ一週間ほどご飯が喉を通らなかった。 (いや本当に。体重3キロほど落ちたんだけど…) まず、問題数が6問に増えた。3時間で6問て。厳しすぎます。 6問になった代わりに問題に日本語が併記されるように。 これぞグローバリゼーションの正しい形である。非常にうれしい。 次に大会日時が例年の10月初頭から7月初頭と大幅に早まった。練習計画が…。 そして、参加チーム数の大幅増加。去年130チームほど、今年200チームほど。 京大からも去年3チームしか出てなかったのだが、今年は9チームに。 皆日本語が好きなのか?それとも宣伝の成果なのか?
前述のとおり6問、日本語併記。
問題の難易度は、
A(非常に易しい)、B(非常に易しい)、C(易しい)、D(やや難)、E(やや難)、F(やや難?)
ぐらいか。まともに考える必要のあるのはDEFだけ。
A(非常に易しい)、B(非常に易しい)、C(易しい)、D(やや難、実装は簡単)、E(並、実装はやや面倒)、F(並)
よくよく考えるとこの程度か。
ちなみにうちのチームはABCDの4問を正解した。
解けたものとか解けなかったものとかの解答。
A〜Dまでは実際にAcceptしたので大丈夫。
E,Fはうちで適当に作った。一応Sample Inputでの正解と
First Input が実用的な速度で解けることを確認している。
以下のソースはすべて正しい結果を出力することを確認している。
(追記:7/22 公式サイトに出力データが公開された)
以下、おのおのについて。とりあえず解いた順で。
カードに特定の操作を繰り返して、一番上に来るものを求める問題。 処理を実装するだけだし、その処理も簡単…なはずだけど、 緊張のあまり配列の添え字に頭が混乱してしまってデバッグに手間取り。 さらにSubmitにも手間取って 結局最初問題が見えるまでかかった5分ほどを含めて20分弱かかった。 緊張している時にこういう問題は要注意である。
マップが与えられて、スタート地点から到達できるマスの数を求める。 塗りつぶすだけ。ものすごい簡単。混乱するタイプの問題でもなかったので 開始から30分目ほどでAccept。ノーデバッグ。
与えられた分数を分子が1の分数で表現する方法の数を求める。 普通の深さ優先探索でOK。時間も十分間に合う。 分数の個数と分母の積に制限があるので、それで枝狩りできる。 普通に書いて普通に速いプログラムが作れた。 開始1時間ほどでAccept。
私が問題ABCを解いている間にK氏に考えてもらっていた問題。 実装を任せて残りの問題を考えに。
問題Fは見た目よりも難しそう。 苦手な幾何問題の問題Dと一緒に見比べながらうんうん唸っていると、 問題Dの方の解き方をひらめいた。問題Cを解いてから30分ほど。 まだまだかかりそうだった問題Eを中断してもらって問題Dの実装。 ちなみに、「情報処理」という雑誌に載っていたプログラムプロムナードという 連載の"丸い紙ふぶき"の回の解法を思い出してこれの解法を思いついた。 そちらも参照してもらえれば参考になるかもしれない。
問題Dは、二次元平面上に複数の点が与えられ、 それらをもっとも多く囲む半径1の円を求める問題。
最初、x,y座標が0から10まで、点は0.0001刻みにしか存在しないということなので、
100000*100000の点を調べてやろうかとも思ったが、さらに最大300個の点との距離比較を
しないといけないので、絶対に間に合いそうに無かった。
2,3の点を含む外接円の中点のいずれかに求める円の中心が存在するとの
M氏の指摘であったが、その中に本当に求める点があるのかどうかの簡潔な
証明が得られなかったので、他の手を考えることにした。
(計算量もO(n^4)なのでn=300に対しては遅すぎるかもしれない)
私が思いついたのは、求める点は与えられた各点を中心とする
半径1の円によって分割される領域の中のひとつに求める点があるということ。
上図にて、0個とかかれたところを中心とする半径1の円には点が0個、 1個なら1個、2個なら2個含まれることが容易に分かると思う。 また、問題の定義から円周上の点も含むので、境界上の点はその境界が接する 領域の中でもっとも多くの数となる。 上のことから、100000*100000の座標全部調べなくても、 点を中心とする半径1の円により分割される各領域の中の点を ひとつづつ調べればそれですべてが尽くされることが分かる。
Fig.1 各点を中心とする半径1の円が平面を分割する
そうと分かればその領域の算出方法である。 最初 "丸い紙ふぶき" で使われている方法をそのまま使った。
まず、円同士の交点と円の上端、下端のy座標をすべて求める(黒点)。 そこを通る水平線を引き(黒線)、隣り合う水平線の中間の線を求める(赤線)。 それぞれの赤線について、円との交点を求める(赤点)。 隣り合う赤点と赤点の中点(緑点)をすべて求めると、 この緑点が全領域を含む。
Fig.2 領域中の点
しかしなんと、遅すぎる。ここまで実装して気づいたが、 オーダはO(n^4)かもしれない。 上誌によると円100個まで解けるらしかったのですっかり安心していた。 (それに、上図で緑点は領域をすべて尽くしてはいるが、重複が多すぎて無駄が多い) 数分考え込んでしまったが、とっさに円の交点だけでOKということに気づく。 この問題は紙ふぶきと違って領域の境界上の点でも正しく答えが求まるのである。 なんというか、紙ふぶきを知っていたがゆえに遠回りをしてしまった感もあるが、 知らなかったらスパッとした解法を思いついていたかどうかは怪しい。 なんともいえないが。
そこまで考えて、急遽アルゴリズムを単純化。 交点を調べるだけのプログラムを作成。オーダーはO(n^3)である。 速度は問題ないレベルに。 ところが答えが合わない。0個とか出力されるケースがある。 0個などありえないわけだが、 問題を見てみると、交点が存在しない場合である。なるほど、確かにおかしくなる。 それを回避するために交点のほかに円の中心自身も調べる候補に追加した。 (これは後で考えると、交点が無いときは1個とするだけで良かった) ということでAccept。途中結構手間取った。最初から交点でよいということに気づいていれば もっともっと速かったはずなのに。 ついでに円の交点ルーチンを求めるルーチンを紙から移すときに 大量のタイプミスをしてしまったせいでそれでも結構かかってしまった。 この時点で2時間半。延長が15分あったので残り45分。 問題Eができることを祈りつつK氏とM氏に残りの実装を任せる。
ついでに問題D解法のまとめ。散文的になってしまったので。
残り45分間、どうせ実装はできないだろうなぁと思いながら適当に考える。
考えれば考えるほど面倒な問題に思えてくる。これは解いちゃいかん問題だった。
縦と横の通りのグルーピング、
方向つきグラフを基にしたランクによるグルーピングとかを行う必要がありそう。
前者はそれほどでないにしても後者がさっと思いつきそうに無い。
適当に全部調べて大丈夫なオーダだろうか?
しかし、どう転んでも45分で実装、デバッグまで完了するのは不可能そうである。
最後に残して正解。
今年は5問がトップだろうなぁと思いつつ。
正直6問解いてるチームがあったら今の私には敵う気がしないと思った。
問題Fが特に難しくないのでは?というK氏の指摘があった。 交差点における強さの関係に同水準関係を追加したグラフを探索すればいいだけという話。 同水準関係は何か再帰的に処理しないといけないと勘違いしていた。 特定の二つの通りの間に何か通りが挟まっているかどうか調べるだけなのな…。 方向が異なるかどうかは、グラフ中でのノード間の距離から判別できると。 なんかよく分からないが、自分でどんどん難しいほうに考えてしまっていたらしい。 というか、問題が全然読めてなかった。本格的に駄目ですな…。 というわけで、Sample Outputと一致するものができた。 探索は最初適当に深さ優先で書いてみたけど、First Inputが全然間に合わなかったので、 距離をテーブルに書き込むダイクストラっぽいものに変更。
任せたきりほとんど何も言及しなかった問題Eであるが、 ようやく気力が回復してきたので解いてみた。 方針はすでに立っていたので実装するだけだったのだが。
Fig.3 容器例
まず、容器を仕切り情報を元に上図のように長方形に分解し、 さらに各長方形をつないでツリーにする。これで準備は完了である。
Fig.4 ツリーへの構成
各ノードは、左端のX座標、右端のX座標、深さ、現在の水量などのパラメタ及び 子ノードへのポインタ、親ノードへのポインタを持つ。 これにより、ノードへ水を注ぐという動作を帰納的に記述することができる。 まず、最初に蛇口のX座標に対応するリーフノードに全水量が流れ込むとする。 リーフノードへの注水は、水がそのノードへ入りきればそれで終了、 入りきらなければ親ノードへ残りの水を注水することになる。 リーフノードではないノードへの注水の場合、まず子ノードを調べる。 子ノードのいずれかが満水で無い場合、そのノードへ注水を丸投げする。 両方満水で無い場合いずれかに投げることになるが、 ここでどちらに注水すればよいか決めることはできない。
このような場合にどちらへ流すか決定できるように、 あらかじめどちらか方向へ伝って流れているという情報を 受け取ることにする。この情報を付加するのは子ノードのどちらか片方だけが 満水になっている場合に、満水で無いノードに丸投げする場合だけであるが、 逆にそれ以外のケースで必要になることは無い。 子ノードが二つとも満水だった場合、リーフノードと同じ処理を行う。
Fig.5 壁伝いにながれるときの例
上記操作を実装すれば、注水作業はノードを上り下りしながら 最終的に全体の水位が決定できる。 蛇口は複数あるが、これは別個に注水している。 注水順が水位に影響を及ぼすことは無いと思われる。 (実は本当かどうかは確かめていない…。直感的にはそう思うのだが) 上記操作で水があふれるけど親ノードが無い。というときは水槽自体から水があふれている ということになるので、特に何もする必要はないだろう。
結局問題Eは解けず。もうちょっと頑張ってくれ… 5問解けなくてひやひやしてたけど、意外なことに4問といてるチームも少なくて 順位は5位。しかし、くやしいくやしいくやしい。 結局トップはまたGokuriか。
Gokuri全問解いてる! 今の私に勝ち目はあるのか? 能力全開状態なら、
ICPCアジア大会国内予選では本番の前に何時間か練習時間がとられる。 基本的にむちゃくちゃ簡単な問題(一番大きい数とか、割り算するだけとか)が出題されるので、 ちゃんと解答が送信できるかどうかなどをテストするのが目的と思われる。
が、最終問題(去年、一昨年は問題E、今年は4問しか無かったので問題D)に まず普通には正解できない問題が出題されるのである。 どういう問題かというと、データとしていくつかの数が与えられていて、 "秘密の数"がそのそれぞれより大きいか小さいかを出力する。 秘密の数?と思われるかもしれないが、これに関するヒントは本当に何もない。 ところがなんと、去年東工大(?)の1チームがこれを解いてた。 そんなこともあって、今年はうちのチームも解いてみることにした。 結果、苦労の末についに正解することができた。 そして同時に、この問題の裏に隠されていた驚愕の事実を知ることとなった。
秘密の数であるが、これに関するヒントは一切与えられていないので、 結局のところ何回もSubmitするしかない。 しかし、闇雲にやっても駄目である。 もっとも効率のいい形で、システマティックにSubmitする必要がある。
まず最初のデータに対する答えをAcceptさせるために必要なSubmit回数は 高々最初のデータ中の数の数であることを理解しなければならない。 実際のデータには100個ほど数が書かれているので実際にはそのぐらいである。 また、Sample Input である 1 9999 に対して Outputが larger smaller となっているので、 秘密の数が2から9998の間にあることが分かる。 これはたいしたヒントではないが… とりあえず、入力データの数と数の間を適当に秘密の数として、 小さいものから順に送りつけてみるのが良いと思う。
上記作業を根性で乗り切ればめでたくデータ1のAcceptである。 次のデータがもらえるが、これがものすごいヒントとなっている。 端的に言えば、このデータにより秘密の数は一通りに決まるようになっていた。 解けない問題と思いきや、しっかり解けるように作られていたのである。 具体的には、一回目のAcceptによりこちらが得られる状況は、 先に送ったソース中の秘密の数が挟まっているデータ1の数の境界 のいずれかが秘密の数であるということ、最初10000個ほどあった候補が 平均的に100個ほどにまで絞られたということになる。 この絞られた範囲についてデータ2を用いて先と同じことをすればよいのだが、 その作業にて候補がひとつになるようになっていたのである。 確かに、データ2がてんでばらばらな数だったら運がそれなりに無いと データ4まで間違えて解答権を剥奪される怖れもある。
というわけで、データ2をもらい、ひとつに決まった秘密の数を入力したソースで Submitし、プログラム変更による再解答送信要求に応じ、データ3の解を送り、 めでたくAcceptと相成った。 私達は、境界計算/秘密の数候補生成、ソース生成、コンパイル、実行、さらにメールの文面生成 までプログラムで行ったが、メールの送受信はよく分からなかったので 手動で行った。M氏が頑張ってくれた。 (まぁ、高々100回なので人がでできないレベルではないと思う)
今年はうちを含め4チームがこの問題を解いてた。 うちのチームは3位だったのだけど、上位2チームは同じ大学で、しかも ペナルティがうちのチームの半分ぐらいづつ。協力しやがったな。 まぁ、練習時間だったから許しといたる。 ということで、来年は皆さんもトライしましょう。 解けない問題に見せかけて、しっかり解ける。 難易度としてはまともに考えると予選本問題の並レベルの問題であろうか。 あとは根気。(あるいはメールプロトコルの知識か…)
ところで "驚愕の事実" だが、データ2が作為的になっているということだけではない。 これはそのものずばり、秘密の数にあり。ネタばれをするつもりは無いが、 頑張って必死にこの問題を解いて、そのおちがこれかよ、みたいな。 主催者様のユーモアセンスをひしひしと感じた一日なのであった。