Hatena::Grouptopcoder

nodchipのTopCoder日記 このページをアンテナに追加 RSSフィード

2017-08-07ICFPC2017 このエントリーを含むブックマーク このエントリーのブックマークコメント

ICFPC2017の参加記録です。以下、nodchipの主観で書いているため、事実と異なる点がある可能性があります。

チームメンバー

今年は現在の同僚・前職の同僚・コンピューター将棋関連の知人の計6名で参加しました。

  • fof
    • 会場提供・通信ライブラリ・アルゴリズム担当
  • nodchip
    • 基本データ構造ライブラリ・アルゴリズム・インフラ担当
  • peria
    • インフラ・テスト担当
  • Qhapaq
    • アルゴリズム担当
  • t-suzuki
    • 通信ライブラリ・ヴィジュアライザ・アルゴリズム担当
  • ynasu
    • アルゴリズム担当

(辞書順・敬称略)

問題概要

periaさんのTweetを引用します。

タイムテーブル

1日目 20:00頃
  • 会場到着。あまりにも豪華な会場だったためメンバー全員ビビる。
1日目 21:00~
  • コンテスト開始。なかなか問題文が公開されない。
  • 問題文公開とともに日本語訳開始。序文を翻訳している間に他のメンバーが問題を読み終える。
  • 翻訳が面倒になったため放置。他のメンバーに問題文を教えてもらう。
  • どうやらグラフ上のエッジを取っていって、ラムダ鉱山からの道を引いていくゲームらしい。
  • 勇み足ぎみにゲームのデータ構造を書き始める。
  • コンピューター将棋い精通したメンバーが多かったため、命名パターンはやねうら王に似せた。

  • 鉱山から拡頂点までの最短距離を求めるダイクストラ法が一発で通らなかった。しょんぼり。

  • 基本データ構造「Position」が完成。Positionを指し手データ構造「Move」に従って更新するPosition::do_move()を実装する。

  • 評価値を計算するEval::evaluate()を実装した。全計算しているので遅い。
  • プレイヤーに取られていないエッジの集合を表すデータ構造を実装。大分前のマラソンマッチで使ったデータ構造の流用。
1日目 00:00~
  • 連結成分をの管理にUnionFindを使った。
  • 指し手を一手戻すPosition::undo_move()を実装するためUnionFindのundoの方法を調べる。
  • 普通に見つかった。縮退をさせず、unionSet()したときに更新された値を全てスタックに保持しておけば良いらしい。
1日目 03:00~
  • 基本ライブラリのパフォーマンスベンチマークを作った。
  • CodeXLを使ってパフォーマンスプロファイルを取ってみた。ベンチマーク自体が一番遅いことが分かった。無かったことにした。
  • メンバーのアドバイスのおかげで、Eval::evaluate()の差分計算の方法を思いついた。
  • 追加するエッジeの両端srcとdstについて、srcを含む連結成分内の全ての頂点とdstを含む連結成分の全ての鉱山の直積集合をとり、それぞれについてスコアを足していけば良い。srcとdstをひっくり返して同様に計算する。
  • 各プレイヤーが取ったエッジからなるグラフを追加で持たせてやれば、時間計算量はならしでdo_move()一回につき鉱山の個数になる(はず)。
  • 就寝…
1日目 08:00~
  • データ構造が色々とバグっているので修正した。
  • undoをしなければならないデータ構造ってバグりやすい気がする…。
  • これっていちおう永続化データ構造と言えるのだろうか…。
  • メンバーがオンラインモードの出力をゲームサーバーとつなげる仕組みを作ってくださったのだがWindowsで動かなかった。
  • 仕方なくC#で再実装した。
1日目 12:00~
  • バグ修正その他
1日目 15:00~
  • 大人気webゲーム、1024の解法アルゴリズムを参考に、反復深化平均最大化法を実装してみた。
  • すでにメンバーが実装していた自己対戦フレームワークを参考に、複数同時対局可能な自己対戦フレームワークを実装した。
1日目 18:00~
  • バグ修正など
  • 一時帰宅
  • Amazon EC2 m4.16xlargeインスタンスを1台スポットでレンタル。WCSC27でElmo瀧澤さんに教えていただいた、入札化価格をオンデマンド価格にするというテクニックを実践してみた。
  • オハイオデータセンターで1時間0.45$くらいで借りられた。ありがたや…。
  • EC2インスタンスにJenkinsを導入した。
  • selfplayブランチにpushするとJenkinsで自己対戦が走り、終了後にチームのSlackの#Jenkinsチャンネルに通知が飛ぶ仕組みを作った。
2日目 12:00~
  • リファクタリング
  • バグ修正
  • AI調整
2日目 15:00~
  • AI調整
  • 自己対戦フレームワーク調整
3日目 21:00~
  • AI調整
3日目 00:00~
  • 自己対戦フレームワーク調整
  • 就寝
3日目 09:00~
  • 仕様変更…。
  • Moveデータ構造を変更し、splurge・optionに対応できるようにした。

3日目 12:00~
  • Moveデータ変更に伴うビルドエラーの修正をした。
3日目 15:00~
  • AIのリファクタリング
  • メンバーが作ってくださったヴィジュアライザをWindowsでも動かせるようにした。

3日目 18:00~
  • ヴィジュアライザで遊ぶ。
  • optionが正しく処理されていないバグを発見…。
  • デバッグ…。
3日目 21:00~

総括

  • 最後に重大なバグが発覚したのが痛かった。次回参加するときはQA体制を充実させたい。
  • 最終的にsubmitしたAIは各頂点のスコアの期待値を計算してGreedyに取るというものらしい。反復深化平均最大化法より強かった。
トラックバック - http://topcoder.g.hatena.ne.jp/nodchip/20170807

2015-12-24

競技プログラマー向け将棋AI開発入門 00:00 競技プログラマー向け将棋AI開発入門 - nodchipのTopCoder日記 を含むブックマーク はてなブックマーク - 競技プログラマー向け将棋AI開発入門 - nodchipのTopCoder日記 競技プログラマー向け将棋AI開発入門 - nodchipのTopCoder日記 のブックマークコメント

はじめに

この記事はCompetitive Programming Advent Calendar 2015 25日目の記事として書かれたものです。

内容は競技プログラミング経験者であるnodchipが将棋のAIの開発を通して得た知識と経験をまとめたものです。

まずは動かしてみましょう

f:id:nodchip:20151224235618p:image

一から将棋プログラムを作るのは難しいと思います。まずは既存の将棋AIを動かしてみて、どんな感じなのか感覚を掴んでみましょう。無料で手に入る主な将棋AIは以下の通りです。

  • Bonanza - The Computer Shogi Program http://www.geocities.jp/bonanza_shogi/
    • 2005年に登場しコンピューター将棋業界に一つのブレークスルーを起こした伝説の将棋ソフトです。
  • GPSshogi - PukiWiki http://gps.tanaka.ecc.u-tokyo.ac.jp/gpsshogi/
    • 東京大学で実践的プログラミングを主催されている金子知適准教授が開発に携わっていることで有名です。第2回電王戦第5局において約680台の端末をクラスタ化してA級プロ棋士三浦弘行九段に勝利したことは記憶に新しいと思います。
  • Apery http://hiraokatakuya.github.io/apery/
    • 平岡拓也氏の開発された将棋AIソフト。第3回電王トーナメントにおいては3位に入賞しています。オープンソースとして公開されています。
  • nodchip/tanuki-bin https://github.com/nodchip/tanuki-bin
    • 上記のソフトをnodchipが改造した物です。

これらのソフトはUSI(Universal Shogi Interface)プロトコルに対応した将棋AIです。

ネット上にはUSIプロトコルに対応したVisualizerがいくつか公開されています。nodchipはVisualizerとして『将棋所』を使用しています。

将棋AIを適切なフォルダに解凍したあと、将棋所→対局→エンジン管理から登録し、対局→対局で対局することができます。AI同士を対戦させるのも良いですし、AIに挑んでボッコボコにされるのも良いかもしれません。

ソースコードをコンパイルしてみましょう

次にソースコードをダウンロードしてコンパイルしてみましょう。Windowsの場合はVisual Studio 2015 Community EditionやMinGW-w64+MSYS、Linuxの場合はgccがあればコンパイルすることができると思います。

tanuki-の場合

  1. ソースコードをgithubよりcloneしてくる
  2. Visual Studio 2015 Community Editionをインストールする
  3. tanuki-/tanuki-.slnを開く
  4. ビルドボタンを押す

これだけです。

将棋ソフトのコンポーネント

将棋のAIは主に以下のコンポーネントに分かれています。

  • 盤面データ構造
    • 局面の状況を保持するデータ構造。強いソフトはBitBoardを用いた高速・省メモリなデータ構造を使っていることが多い。BitBoardについては後述。
  • 指し手生成
    • ある局面が与えられた時に、手番のプレイヤーが指すことができる合法手を列挙するルーチン。これが高速だと単位時間に読める局面が多くなる。一方、実際に指されそうな手から生成したほうが、ゲーム木探索で枝刈りが多く起こるようになり、探索効率が上がる。高速化と指し手の生成手順は、多分トレード・オフの関係にあると思う。
  • 盤面評価
    • ある局面が与えられた時に、先手後手のどちらが有利なのか数値評価するルーチン。これが高速だと単位時間に読める局面が多くなる。一方、より正確な数値評価ができたほうが強くなる。これもきっとトレードオフの関係にあると思う。
  • 盤面探索
    • ゲーム木を探索していくルーチン。
  • 置換表
    • 局面のハッシュをキー、評価値を値としたキャッシュ。有り体に言うと置換表付き盤面探索はメモ付き探索。局面の評価値と探索の評価値を分けてキャッシュするソフトもある。Apery/大樹の枝/tanuki-は分けてキャッシュしている。
  • 定跡データベース
    • 序盤の指し方を何らかの方法でキャッシュしたもの。オフラインで作成しておくことがほとんど。プロの棋譜から作るもよし、Ponanzaのようにモンテカルロ木探索を用いて作るもよし。
  • USIプロトコル
    • GUIと通信をするための部分。標準入出力にテキストを流していくだけの簡単なお仕事。競技プログラミングで入出力に慣れていれば単なる実装ゲー。ただしI/Oのflushは忘れずに。

ゲームAI独特のアルゴリズムを覚えましょう

コンピューター将棋では競技プログラミング界隈であまり目にしないアルゴリズムが多いです。以下にそれらの一部を挙げます。

改造してみましょう

気になったところを改造してみましょう。プロファイラを使ってホットスポットを特定し、定数倍の高速化をかけるもよいでしょう。探索ルーチンの枝刈りパラメーターを調整するのも良いでしょう。機械学習ルーチンにナウなヤングにバカウケの最先端の学習アルゴリズムを導入するのも良いと思います。

例えばtanuki-がAperyに対して施した改造は以下のとおりです。

  • 定石データベースを変更し、インターネット上で入手可能なプロの棋譜約4万局と、floodgate上の対戦の中でレーティングがGPS Xeon 12コア以上のAIが含まれた対戦の棋譜から作成しなおしました。
  • 定跡選択ルーチンを変更し、データベース作成時は指し手の評価は行わず、本番中に軽い探索を行い、定跡データベースから選択するようにしました。
  • 盤面評価関数のうちKKPをVGATHERDD命令を使って計算するようにしました。
  • volatile変数をstd::atomic<>へ変更しました。
  • static const変数をconstexprへ変更しました。
  • 置換表のEntrySizeを1に変更しました。
  • 置換表・評価値キャッシュの使用率・ヒット率・破棄率を出力できるようにしました。
  • コンパイラをclang-3.7に変更しました。
  • Aspiration Window Searchのwindowの広げ方をStockfish6のものに近づけました。
  • 思考時間を変更し、序盤を短め、中盤を長めにしています。
  • KPPの重みを保持する3次元配列に適当なパディングを入れました。

自己対戦をしてみましょう

将棋所には自己対戦機能が実装されています。これを使って、昔のプログラムに比べて今のプログラムがどれくらい強くなったか確認してみましょう。

まず「対局」→「エンジン管理」から対戦させたいAIを登録します。次に「対局」→「対局」から先手と後手のAIを選択しましょう。残りのオプションをお好みで設定したら自己対戦開始です。おすすめの設定は以下のとおりです。

  • 手数が256手に達したら引き分けにする
  • 時間切れを切れ負けにする オン
  • 連続対局 オン
  • 連続対局数 9999
  • 自動棋譜保存 オン

対局数が少ないとランダム要素のせいで誤差が大きくなり、どれくらい強くなったのか正確に測ることができません。以下は「コンピュータ囲碁 ―モンテカルロ法の理論と実践―」に書かれている、対戦数と有意差の関係です。

試合数有意に強くなったといえる勝率(95%)有意に強くなったといえる勝率(99%)
108勝2敗9勝1敗
2014勝6敗16勝4敗
5031勝19敗34勝16敗
10059勝41敗62勝38敗
200112勝88敗117勝83敗
500269勝231敗277勝223敗
1000527勝473敗537勝463敗

他のAIと対戦させてみましょう

AIが十分に強くなったらネット上で他のAIと対戦させてみましょう。コンピュータ将棋対局場「floodgate」は日本で最も有名なコンピューター将棋ソフト同士の対局場の一つです。プロ棋士の一部も棋譜を参考にしているとのことです。

将棋所からfloodgateに参戦するためには「対局」→「サーバ通信対局(floodgate)」から対戦させたいAIを選び、ログイン名にランキングに表示されるAI名、パスワードに任意の文字列を入力してOKボタンを押せばよいです。

大会に出場してみましょう

現在定期的に開催されている大会は以下のとおりです。

世界コンピュータ将棋選手権は毎年5月のゴールデンウィークに開催されるイベントです。世界と名前が付いている通り、海外からの参加者もいます。

電王トーナメントは株式外社ドワンゴが主催するイベントで、不定期で開催されています。第3回電王トーナメントでは、優勝するとプロ棋士の代表と対戦することができました。

まとめ

競技プログラミング経験者nodchipが将棋のAIの開発を行った経験を元に、将棋のAIの開発に必要な雑多な知識をまとめました。あまりまとまっていない記事で恐縮です…。

この記事を呼んで将棋のAIの開発を始めてくださる競技プログラマーが一人でも増えたら幸いです。

リンク

最後に

これにて競技プログラミングアドベントカレンダー2015は終了となります。それでは良いお年を。

トラックバック - http://topcoder.g.hatena.ne.jp/nodchip/20151224

2015-08-16

AtCoder Regular Contest 043 06:01 AtCoder Regular Contest 043 - nodchipのTopCoder日記 を含むブックマーク はてなブックマーク - AtCoder Regular Contest 043 - nodchipのTopCoder日記 AtCoder Regular Contest 043 - nodchipのTopCoder日記 のブックマークコメント

A - 点数変換

  • 最大値と最小値の差を調整するためにPを計算
  • そのあと平均を計算するためにQを計算
  • コーナーケースは全員が同じ点数かつB>0のとき
int main() {
	std::ios::sync_with_stdio(false);

  int N;
  double A, B;
  cin >> N >> A >> B;
  vector<double> S;
  REP(n, N) {
    double s;
    cin >> s;
    S.push_back(s);
  }

  double min = *min_element(ALL(S));
  double max = *max_element(ALL(S));
  if (max - min < EPS && B > 0) {
    cout << -1 << endl;
    return 0;
  }

  double P = B / (max - min);
  double Q = -accumulate(ALL(S), 0.0) * P / N + A;
  printf("%.20f %.20f\n", P, Q);
}

B - 難易度

  • DP苦手
  • これは蟻本に載っている累積和をとっておくとオーダーがひとつ下がるやつだと思う
  • とりあえず書いてみる
  • サンプルが合わない
  • 累積和取らないバージョンも書いてみる
  • なかなかサンプルが合わない
  • やっと合った…
  • 元のコードは初期化がバグっていたらしい
  • サンプル通った
  • 提出
  • AC
  • DP本当に苦手
typedef G<1000000007> GG;

int main() {
	std::ios::sync_with_stdio(false);

  int N;
  cin >> N;
  vector<int> D;
  REP(n, N) {
    int d;
    cin >> d;
    D.push_back(d);
  }
  sort(ALL(D));

  //static GG dp[4][128 * 1024];
  //REP(i, 128 * 1024) {
  //  dp[0][i] = 1;
  //}

  //REP(i, 3) {
  //  REP(j, N) {
  //    REP(k, j) {
  //      if (D[k] * 2 <= D[j]) {
  //        dp[i + 1][j] += dp[i][k];
  //      }
  //    }
  //  }
  //}
  //cout << accumulate(dp[3], dp[3] + 128 * 1024, GG()).x << endl;

  vector<GG> dp(N, 1);
  vector<GG> acc(1);
  REP(n, N) {
    acc.push_back(dp[n] + acc.back());
  }

  REP(i, 3) {
    vector<GG> nextDp;
    for (const auto& d : D) {
      int index = distance(D.begin(), upper_bound(ALL(D), d / 2));
      nextDp.push_back(acc[index]);
    }
    vector<GG> nextAcc(1);
    REP(n, N) {
      nextAcc.push_back(nextDp[n] + nextAcc.back());
    }

    swap(dp, nextDp);
    swap(acc, nextAcc);
  }
  cout << acc.back().x << endl;
}

C - 転倒距離

  • 分かりませんでした

D - 引っ越し

  • 10点は全部試すだけ
  • TLE 10
  • 100点は分かりませんでした
int main() {
	std::ios::sync_with_stdio(false);

  int N, M;
  cin >> N >> M;
  vector<int> P;
  REP(m, M) {
    int p;
    cin >> p;
    P.push_back(p);
  }
  P.resize(N);
  sort(ALL(P));

  int answer = 0;
  do {
    int sum = 0;
    for (int j = 0; j < N; ++j) {
      for (int i = 0; i < j; ++i) {
        sum += P[i] * P[j] * (j - i);
      }
    }
    MAX_UPDATE(answer, sum);
  } while (next_permutation(ALL(P)));
  cout << answer << endl;
}

結果

順位ユーザ名点数変換難易度転倒距離引っ越し得点 / Total
24nodchip100 06:19100 (1) 56:54-10 38:13210 (1) 61:54

目標の3完には届きませんでした。

トラックバック - http://topcoder.g.hatena.ne.jp/nodchip/20150816

2015-07-23

Codeforces Round 313 10:22 Codeforces Round 313 - nodchipのTopCoder日記 を含むブックマーク はてなブックマーク - Codeforces Round 313 - nodchipのTopCoder日記 Codeforces Round 313 - nodchipのTopCoder日記 のブックマークコメント

A. Gerald's Hexagon

  • いきなり面倒そうな問題
  • 三角格子が出てきた時はとりあえず正方格子に変換することを考える
  • 正方格子に変換すると長方形から対角上の二等辺直角三角形を2つ取り除いた図形になる
  • 各頂点の座標は元の六角形の座標からすぐに分かる
  • ならば面積を求められるか・・・
  • Accepted
int main() {
	std::ios::sync_with_stdio(false);

  int x0 = 0;
  int y0 = 0;

  int length;
  cin >> length;
  int x1 = x0 - length;
  int y1 = y0 + length;

  cin >> length;
  int x2 = x1 - length;
  int y2 = y1;

  cin >> length;
  int x3 = x2;
  int y3 = y2 - length;

  cin >> length;
  int x4 = x3 + length;
  int y4 = y3 - length;

  cin >> length;
  int x5 = x4 + length;
  int y5 = y4;

  cin >> length;
  int x6 = x5;
  int y6 = y5 + length;

  assert(x0 == x6);
  assert(y0 == y6);

  int area = (x5 - x2) * (y2 - y5) * 2;
  area -= (x4 - x3) * (x4 - x3);
  area -= (x1 - x0) * (x1 - x0);
  cout << area << endl;
}

B. Equivalent Strings

  • これ再帰するだけに見えるんだけど・・・
  • CodeForcesだから絶対何かあるはず
  • ・・・
  • よく分からないorz
  • とりあえず枝刈りだけでも入れておこう
  • 部分文字列に含まれる文字の種類と数が違っていたらrejectすることにする
  • Accepted
int counterA[256 * 1024][32];
int counterB[256 * 1024][32];
string a, b;

bool equals(int beginA, int endA, int beginB, int endB) {
  if ((endA - beginA) & 1) {
    return equal(a.begin() + beginA, a.begin() + endA, b.begin() + beginB);
  }

  int diffA[32];
  REP(i, 26) {
    diffA[i] = counterA[endA][i] - counterA[beginA][i];
  }
  int diffB[32];
  REP(i, 26) {
    diffB[i] = counterB[endB][i] - counterB[beginB][i];
  }

  if (!equal(diffA, diffA + 26, diffB)) {
    return false;
  }

  return (equals(beginA, (beginA + endA) / 2, beginB, (beginB + endB) / 2) &&
    equals((beginA + endA) / 2, endA, (beginB + endB) / 2, endB)) ||
    (equals(beginA, (beginA + endA) / 2, (beginB + endB) / 2, endB) &&
    equals((beginA + endA) / 2, endA, beginB, (beginB + endB) / 2));
}

int main() {
	std::ios::sync_with_stdio(false);

  cin >> a >> b;
  REP(i, a.size()) {
    copy(counterA[i], counterA[i] + 26, counterA[i + 1]);
    ++counterA[i + 1][a[i] - 'a'];
    copy(counterB[i], counterB[i] + 26, counterB[i + 1]);
    ++counterB[i + 1][b[i] - 'a'];
  }

  cout << (equals(0, a.size(), 0, b.size()) ? "YES" : "NO") << endl;
}

C. Gerald and Giant Chess

  • げ・・・分からない・・・
  • 座標圧縮でもするのだろうか・・・
  • DPと数え上げ苦手orz

D. Randomizer

  • 幾何っぽいと思ったら全然わからなかったorz

E. Gerald and Path

  • とりあえず焼きなまし送りつけておく
  • Pretest通っちゃった
  • まぁいいや
  • Wrong answer on test 15
  • N=4のテストケースで引っかかってる。
  • なんでだろう・・・

結果

#Who= *A 500B 1000C 1500D 2250E 2250
329Japan nodchip1336 480 00:10856 00:36 -1

1951 -> 2018 普通の成績だったにも関わらずレーティングが上がりました。とりあえず現状維持が目標です。

トラックバック - http://topcoder.g.hatena.ne.jp/nodchip/20150723

2015-06-13

AtCoder Regular Contest 040 22:30 AtCoder Regular Contest 040 - nodchipのTopCoder日記 を含むブックマーク はてなブックマーク - AtCoder Regular Contest 040 - nodchipのTopCoder日記 AtCoder Regular Contest 040 - nodchipのTopCoder日記 のブックマークコメント

A - 床塗り

  • 数える
int main() {
	std::ios::sync_with_stdio(false);

	int N;
	cin >> N;
	int takahashi = 0;
	int aoki = 0;
	REP(n, N) {
		string s;
		cin >> s;
		takahashi += count(ALL(s), 'R');
		aoki += count(ALL(s), 'B');
	}
	if (takahashi > aoki) {
		cout << "TAKAHASHI" << endl;
	} else if (takahashi < aoki) {
		cout << "AOKI" << endl;
	} else {
		cout << "DRAW" << endl;
	}
}

B - 直線塗り

  • 貪欲法
    • 自分がいるマスが塗られていないならインク発射
    • 一番右側の塗られてないマスに届くならインク発射
    • そうでないなら右移動
int main() {
	std::ios::sync_with_stdio(false);

	int N, R;
	string S;
	cin >> N >> R >> S;

	int time = 0;
	int current = 0;
	while (S.find('.') != string::npos) {
		++time;
		if (S[current] == '.') {
			fill(S.begin() + current, min(S.begin() + current + R, S.end()), 'o');
			continue;
		}

		int last = S.find_last_of('.');
		if (current + R > last) {
			fill(S.begin() + current, min(S.begin() + current + R, S.end()), 'o');
			continue;
		}

		++current;
	}


	cout << time << endl;
}

C - Z塗り

  • 貪欲法
  • 上から下、右から左に見ていき、塗られていないマスを見つけたらインク発射
vector<string> table;

void fill(int r, int c) {
	for (int j = c; j >= 0; --j) {
		table[r][j] = 'o';
	}
	if (r + 1 < table.size()) {
		for (int j = c; j < table.size(); ++j) {
			table[r + 1][j] = 'o';
		}
	}
}

int main() {
	std::ios::sync_with_stdio(false);

	int N;
	cin >> N;
	REP(n, N) {
		string s;
		cin >> s;
		table.push_back(s);
	}

	int answer = 0;
	for (int r = 0; r < N; ++r) {
		for (int c = N - 1; c >= 0; --c) {
			if (table[r][c] == 'o') {
				continue;
			}
			fill(r, c);
			++answer;
		}
	}
	cout << answer << endl;
}

D - カクカク塗り

  • 分かりませんでした

結果

順位ユーザ名床塗り直線塗りZ塗りカクカク塗り得点 / Total
20nodchip100 04:45100 13:37100 21:45(1)300 21:45

久々に1ページ目に載ることが出来ました。しばらくは現状維持が目標になりそうです。

トラックバック - http://topcoder.g.hatena.ne.jp/nodchip/20150613