プログラムコンテスト

ysd@KLab > プロコン

リスト

HAL研プログラミングコンテスト2005

トリックコード

恐らくこれと同様なことをした人は、34万点というスコアを叩き出すも消されるだろうというコード。提出してサーバに負荷をかけてはいけません。
#include "check.h"
namespace Problem {
  bool makeRoute(const BridgeList& bridgeList, Route& route)
  {
    static int numProblem = 0;
    if( numProblem < 100000 ) {
      numProblem++;
      // 普通のプログラムを書く。ただし、20000程度のスコアを取る速度は必要かと思われます。
      // return 文まで含める。
    }
    return false;
  }
}

アルゴリズム

オイラーの定理

そもそもオイラーの定理とは、与えられた無向グラフで一筆書きできるかできないか(true/false)を見分ける定理です。 まず、エッジ(橋)が奇数本出ているノード(島)を数えます。そのノード数が0または2の場合に限り、与えられたグラフは一筆書きができます。 そして一筆書きができるグラフ(=エッジ+ノード)をオイラーグラフと言います。Konigsbergの橋の逸話もセットで覚えましょう。

今回の問題では最大で2本の一方通行橋がありますが、いずれの場合も[全ての橋を渡りきる⇒オイラーグラフ]が成り立っています。 つまり対偶を取れば[オイラーグラフでない⇒false]なので、各島にかかる橋の数を数えるだけでfalseになる物の大半を篩い落とせます。

Fleury (フラーリー) のアルゴリズム

これは一筆書きの具体的なルートを決めるアルゴリズム。具体的な中身は文献によって少しずつ異なるようなので、ここでは自分が実装に使った形で書く。

BridgeList bridgeList; // 橋のリスト。問題で与えられたやつ。
int        steps = 0;  // 渡る島を何個記録しているか。

void fleury(int island)
{
  for( i = 0 ; i < bridgeList.size() ; ++i ) {
    if( bridgeList[i].isletA == island ||
        bridgeList[i].isletB == island   ) {      // 渡れる橋が見つかった場合
      int nextIsland = bridgeList[i].isletA + bridgeList[i].isletB - island; // 対岸の島番号を記録
      bridgeList.erase( bridgeList.begin() + i ); // 今渡った橋を消す
      fleury(nextIsland);                         // とりあえず渡る
    }
  }
  
  // island番目の島から移動できなくなったらその島を記録
  route.set(steps++, island);
}

言葉で表すなら「とりあえず渡れる橋を渡って、渡れなくなったら記録して1歩前に戻る、の繰り返し」という感じ。

各橋に対する参照が1回ずつなので、時間オーダは O(橋数)。

Grid Challenge 2006

問題作成コード(自己責任でお使いください)

makeprob.c

解答用プログラムを作る前なのに、練習用の問題数が足りないと思って問題を作るプログラムを組んでしまいました。使い方は

$ ./makeprob <ノード数> <平均接続数>
です。標準出力に出力されるので、リダイレクトでファイルに落とす必要があります。

パラメータに関してですが、ノード数はそのままの意味で、平均接続数とは1つのノードに繋がるエッジの平均数です。 プログラムの中身としては全体で (ノード数)*(平均接続数)/2 本のエッジをランダムにかけているだけです。

実験してみた結果、元のサンプル問題と同じ程度の難易度の問題にするには

元ファイルノード数平均接続数
01.dat500 6
02.dat50010
03.dat50020
04.dat50035
あたりのパラメータが近いようです。が、パラメータは自由に設定してください。上記のはあくまで参考。

(自分が使った)アルゴリズム

通信関係

GXPの標準入出力を使った通信だと、何が原因か、ある程度以上の長さ(改行を区切りと考えて)のデータを送信しようとすると止まってしまうのでソケットを張る練習を兼ねてやってみました。
  • マスターはTCPソケットのサーバとして準備する
  • GXPでマスターのホスト名をその他全員に知らせる
  • GXPで飛んできたホストにTCP接続する
  • あとはそのTCPを介して通信する

解法関係

ワーカー役の901CPU( 実行時に生きてた数)は2スレッドにして、一方は解を探索し続け、一方は通信を行ってます。
解法部分は基本的にサンプルプログラムのまま、公式ページにあった論文の ClearCount に関するループを入れました。
while(時間が来るまでループ) {
  初期解を作る
  for( clearcount = 0 ; clearcount < clearcountの上限となるパラメータ ; clearcount++ ) {
    解を更新する(サンプルプログラムとほぼ同じ)
    score = 新解のカット数を数える
    if( 自己ベスト < score ) {
      最良解を更新する
      clearcount = 0;
    }
  }
  if( 知っている中での最小カット数 > 自己ベスト ) {
    マスターに自己最良解を送信
    自己ベストのカット数をGXPで知らせる
  }
}
通信部分は、GXPの入力から他ノードから最良解のカット数を知り、該当変数を更新してます。特定の文字列が来たら終了するという判断もこっちで行ってます。
マスターも2スレッド。
一方はひたすら select で通信が来るのを待つ。解が届いたらAPIに食わせて記録を更新します。
while(無限) {
  解を得る();
  AnswerAPI();
}
もう一方はずっと時間を監視。かといってCPUを食いつぶしては仕方ないので 10μs 休む命令も加えてみたり。
while( 時間 < 制限時間 ) {
  usleep(10);
  時間取得API();
}

感想

つまりは個人的な意見。思いつきやすいのはやっぱりネガティブな方。
  • 実は問題の設定が曖昧だった。2ノード間のエッジは最大1本(要はウェイトが無い)ということは明記されていない。
  • 座標に意味はあったのか?利用するかどうかは別として。
  • APIが貧弱。公開APIだと問題番号1~4しか対応せず、予選用APIだと11~14しか対応せず、決勝用APIだと21~24しか対応しない。例えば公開APIならどの番号でも ./xx.dat を返すように、決勝用なら 11~14 は予選用のパスを返すようにして欲しかった。実験と本番でライブラリの位置を変えなきゃならないのは面倒。
  • 以下、自分の反省点。
  • 1対901で通信やってると、データの受け渡しだけで5分前後かかっちゃう。せめてクラスタ毎にでも分けていれば1分強くらいで終わったろうに。理想はクラスタ毎に、中では二分木。
  • とか思っていたら、全NFSでデータのあるPathが共通だったため、それを通信して各Nodeで読み込めばOK、だったらしい…orz
  • 解の探索プログラムはもうちょい色々できた気がする。

ACM/ICPC 2006 国内予選

今年のICPC国内予選、私は選手としての資格がないのでコーチ役で参加。コーチしたチームは負けました。東大の壁は厚い…とか言えるレベルだったらよかったんですが。

一応、何かしら参考になるかなぁという程度に作ったプログラムを置いてみます。作成時間や実行時間よりも、解読しやすいを主眼に置いてます。コメントが無いですけど…。

A ディリクレの算術級数定理

icpc2006A.cc

超易問。0問正解のチームは、そもそもやっていないか、Submitがうまくいかないかの2択だと思います。大雑把に言って2通りの解答方法が挙げられます。

まずは素数判定用の関数を作り、a,a+d,a+2d,… と順番に素数判定にかけていく方法。大半の人はこの方法でやったんじゃないでしょうか。そんな流れで作ったソースを載っけました。

もう一つは、数の範囲が100万未満ということから、エラトステネスの篩でも使って100万未満の True/False 表を作っておき、素数判定を表引きで行う方法。最初の表を作るのに多少時間がかかるかも知れないが、Inputデータが極端に多い場合はこちらの方が総時間としては速いはず。優勝したMakegumiはこちらの方法だった。

B 列車の編成パートII

icpc2006B.cc

易問。でも自分は'+'を文字列に含むんだと勘違いしてTimeOverした。

C++で書いている場合にはstirngクラスとSTLのsetが役に立つ典型例。1:n-1~n-1:1に切り分け、前半、後半、両方を逆転させて再接続した文字列を詰め込んでいくだけ。Cで作っていると以前作ったものとの重複を管理するのが面倒になる。Javaだと…知らない。

C 六角沼の六角大蛇

icpc2006C.cc まだ。

難問。見かけ倒し…というと語弊があるが、実はパッと見で想像する程には難しくない。ただコーディング量がかなり多くなる。

基本的な考え方は詰めオセロみたいな感じ。初期状態から動かせる範囲を列挙→そこから動かせる範囲を列挙→…みたいに繰り返す。接続が切れる条件や死ぬ条件を考えると意外に探索範囲が狭いのでこんな横型探索でも十分行けるはず。ただし重複の可能性があるのでC++のsetをお勧めしたい。

問題なのは、1ステップ毎のループと、どの節を動かすかというループがあること。混乱せずに解いて欲しい。

D カーリング 2.0

icpc2006D.cc

普通。慌てず、騒がず、きちんとシミュレートすること。

私の実装では、再帰呼び出しをする際に石の現在地と何手目なのか、という情報だけ渡している。しかも縦型全探索。氷が隣接していたり石が落ちたりして意外に枝狩りが頻繁におきるらしい。

バックトラックする際に消した氷を元に戻すことを忘れないよう気を付ければ難しくないと思う。

E 全宇宙生命ゲノムデータベース

icpc2006E.cc めっさ醜い。

普通問。単純に、与えられたままに文字列を展開しようとすると(サンプルの4問目とかで)メモリが足りなくなること必至。そこで別の工夫が必要となる。

リスト構造を作るのが本質的な解答だろうか。ループ用と文字列用の2種類(を内部で見分けられる)構造体を用意し、文字列をパージングして後はひたすらツリー構造を辿りまくる。

その際に、ループをどう処理するかが時間的に問題になってくる…はず。単純に1文字ずつ追っていって間に合うのかな?十分間に合います。想像で言うが、ループ用構造体ではループ内の文字列長を記憶しておくと速く処理できる気がする。

上においている実装では構造体を作らずstringクラスを事ある毎にコンストラクト&デストラクトしているので見にくく、遅くなっています。

F 影の秘密

超難問、と言われているがどうなんだろう。実際に与えられる問題データを試していない(下記で述べる方法でAccept取れるなら)私からの印象としては、CやEと同レベルの難易度。とりあえず問題データを通してみたい。正解するプログラムじゃなければタダの机上の空論だから。

解析的な解法が思いつかないので数値解を兎に角やってみる。2案。

どちらの手法も、基本的には角度θによる長さを与える関数f(θ)が連続、かつ微分結果がでかい区間は無いものという想定がある。要するに10-3π程度に小刻みに探索すれば大丈夫だろう、という前提だ。

…とかいろいろアルゴリズムを考えてたけど、本当に重要なのはそこではなく有効桁数では?ということに思い至った。実際、double→long doubleに変更したら型以外は全く同じプログラムで成功している。θに関する精度が求められているのに、比較すべきなのが影の長さというのでdoubleの有効桁数(15桁ちょい)とεの差が消えたのでは?

1/10000スケールで段々狭く…案

icpc2006F.cc

まず探索区間を1000分割して最大値を取るθを求め、あとは再帰的に先程のθの周りをもっと細かい単位(具体的には刻み幅が1/1000倍)になって規程の精度まで見積もる。

最大値を探し終えたら最小値も同様に探す。

計算回数は1問当たり1000×4(再帰の深さ)×2(最大と最小)=8000くらい。現実的?

局所解から全て正確に求めよう…案

icpc2006F2.cc まだ。

まずは↑の案と同様に1000分割していき、前から順に局所解を探す。局所解が見つかったらその周囲で同様に刻み幅を小さくしていき、正確な局所解とその状態での影の長さを得る。

極大と極小でバラバラに記録を保持していけば、1回のトレースで探索が完了するはず。1000×4×n(局所解の数)=4000n。多分現実的。

HAL研プログラミングコンテスト2007

トリックコード

試しにこれを出したら766428を出して消されたコード。やり方は不正だけど,まるっとコピーなので理論的な最速値を示してくれている。投稿したのは初期の混んでなかった頃なので許して。
#include "answer.h"
#include "check.h"

typedef struct Board Board;
void answerCrossWord(Board* board)
{
  int i, j, size = board->size;
  Board* ans = board - 1; // 配布コードでは ans = board + 1;
  for( i = 0 ; i < size; i++ ) {
    for( j = 0 ; j < size ; j++ ) {
      board->cell[i][j] = ans->cell[i][j];
    }
  }
}