JOI2018/2019 予選参加記

結果

ABCDEfの5完506点 DEがやけに簡単だと思いました

始まる前

りゅうおうのおしごと!を見ていた これ泣いちゃうんですよね

問題

全部GitHubにあります

github.com

D

まあソートしてはいなんですけど、全部0に気づかなくていろいろ実装を直したので汚い

int main(){
    ios_base::sync_with_stdio(false);
    cin.tie(0);

    i64 n;
    cin >> n;
    vector<i64> a(n);
    for(i64 i = 0; i < n; ++i){
        cin >> a[i];
        if(i != 0 && a[i] == a[i - 1]){
            i--; n--;
        }
    }
    if(n == 1 && a[0] == 0){
        cout << 0 << endl;
        return 0;
    }
    vector<P> sorted;
    for(i64 i = 0; i < n; ++i){
        sorted.push_back(P(a[i], i + 2));
    }
    sort(sorted.begin(), sorted.end(), greater<P>());
    unordered_map<i64, vector<P>> mp;
    i64 cnt = 0;
    for(i64 i = 0; i < n; ++i){
        if(i == 0){
            mp[cnt].push_back(sorted[i]);
        }else{
            if(sorted[i].first == sorted[i - 1].first){
                mp[cnt].push_back(sorted[i]);
            }else{
                cnt++;
                mp[cnt].push_back(sorted[i]);
            }
        }
        if(i == n - 1 && mp[cnt][0].first != 0){
            cnt++;
            mp[cnt] = {P(0, 1), P(0, n + 2)};
        }
    }
    vector<bool> used(n + 10, false);
    i64 tmp, ans;
    tmp = ans = 0;
    for(i64 i = 0; i < cnt + 1; ++i){
        for(i64 j = 0; j < mp[i].size(); ++j){
            i64 num = mp[i][j].second;
            if(used[num - 1] && used[num + 1]) tmp--;
            else if(!used[num - 1] && !used[num + 1]) tmp++;
            used[num] = true;
        }
        ans = max(ans, tmp);
    }
    cout << ans << endl;
}

E

DPパートが簡単だったのですんなりいった ソートをしましょう

int main(){
    ios_base::sync_with_stdio(false);
    cin.tie(0);

    i64 n, m;
    cin >> n >> m;
    vector<i64> a(n + 10), l(m), r(m);
    vector<P> sorted(m);
    for(i64 i = 0; i < n; ++i) cin >> a[i + 1];
    for(i64 i = 0; i < m; ++i){
        cin >> l[i] >> r[i];
        sorted[i] = P(l[i], r[i]);
    }
    sort(sorted.begin(), sorted.end());
    vector<i64> min_index(n + 10, -1);
    i64 cnt = 0;
    for(i64 i = 0; i < m; ++i){
        cnt = max(cnt, sorted[i].first);
        while(sorted[i].second >= cnt){
            min_index[cnt] = sorted[i].first;
            cnt++;
        }
    }
    vector<i64> dp(n + 10, 0);
    for(i64 i = 1; i < n + 1; ++i){
        if(min_index[i] == -1){
            dp[i] = dp[i - 1] + a[i];
        }else{
            dp[i] = max(dp[i - 1], dp[min_index[i] - 1] + a[i]);
        }
    }
    //for(i64 i = 1; i < n + 1; ++i) cout << i << ": " << min_index[i] << endl;
    cout << dp[n] << endl;
}

感想

今年も本選に行けそうなので嬉しい 真面目に精進して春合宿行くぞい

CyberRebeat CTF Write-up

概要

生活習慣崩壊ズとして出ました 全完して同率1位でした f:id:kobaryo222912:20180909113313j:plain

先に↓のチームメンバーのWrite-upを読むことをおすすめします keymoon.hatenablog.com

ecasd-qina.hatenablog.com

僕が解いたのは6問でやるだけ問題を除くと実質2問+αでした f:id:kobaryo222912:20180909113328j:plain ↑zohe君が解いた問題をぼくとkeymoonがそれぞれ1つずつ代理で提出しています(どちらともBinary) 以下に解いた問題を載せます(解いた順、ジャンルの右の数字はdifficulty)

Write-up

Readme(Misc, 100)

f:id:kobaryo222912:20180909035858j:plain 上の画像が与えられます ぐっと睨んで感じるとflagが出ます(コツは日本語を忘れることです)

Secret.pdf(Stegano, 10)

黒塗り部分があるpdfファイルが与えられます Ctrl+A Ctrl+C Ctrl+V

Whitepage(Web, 10)

<form action="index.php" method="post">
   <input type="text" name="id" style="visibility:hidden" />
   <input type="text" name="password" style="visibility:hidden" />
   <button>LOGIN</button>
</form>

このままだと入力フォームが見えないのでhtmlを書き換えましょう 最初出題ミスでidが"Hiro"じゃなくて"Hero"になっていた 少し時間を溶かされた

Alpha(Stegano, 100)

GIMP

f:id:kobaryo222912:20180909045738p:plain

えいw

f:id:kobaryo222912:20180909045902p:plain

Uploader(Web, 100)

f:id:kobaryo222912:20180909041957p:plain ファイルの検索ページが与えられます

f:id:kobaryo222912:20180909042050p:plain 適当に試すとSQL Injectionっぽいです URLとエラーメッセージからSQLite3っぽいことが分かります 定番の

' OR 1=1;

を試すと f:id:kobaryo222912:20180909042210p:plain

secret.zipが出てきます これを落としてunzipするとパスワードを要求されます guestでログインするとsamle.zipのパスワードが表示されることから、haradaでログインしてパスワードを表示させたくなります これにはharadaの(アカウントの)パスワードが欲しくなります 最初えかすどがBlind SQL Injectionっぽいと言っていてテーブル名がほしいと言われたので頑張ります これはSQLiteのマスターテーブルの情報を検索結果の後につけてあげると通って、

' union select name, sql from sqlite_master --";

としてあげると

f:id:kobaryo222912:20180909042253p:plain

と言われるので0でパディングしてあげます

' union select name, sql, 0, 0 from sqlite_master --";

とすると

f:id:kobaryo222912:20180909042500p:plain

とテーブル名とCREATE TABLEしたときの情報が見れます ここで冷静に考えるとテーブル名を取る要領でUsersの情報が取れそうなことが分かるので、

' union select userid, password, 0, 0 from Users --";

としてあげるとパスワードが

f:id:kobaryo222912:20180909042313p:plain

出てくるのでログインするとzipのパスワードが分かります 解凍すると平文のflagが出てきます (SQL Injectionの問題を初めて解くことが出来たのですごく嬉しかった)

FLAG.encrypted(Crypto, 200)

とりあえず暗号文と公開鍵を見てみます 暗号文は特に特徴は無いですが、公開鍵をopenssl public-key.pemとかして出してみると明らかにeの値が大きいことが分かります ググるとeの値が極端に小さいor大きい時に使えるWiener's attackというものが出てきます

github.com

GitHubにサンプルがあるので落とします 適当に書き換えてあげるとdが出てきます あとはpow(C, d, n)とすると出てきます 最初暗号文を取り違えていてつらかった

番外編

f31337(Binary, 200)

99%zoheが解きます ただ出てきたflagが不正解になるので見てみます

CRCTF{y0ur_m4chine_1s_v3ry_f3st!!}

天才なので見た瞬間に'f3st'の違和感に気づきます まあダメ元で出してみるかと思い

CRCTF{y0ur_m4chine_1s_v3ry_f4st!!}

としてあげると通ります

Signature(Crypto, 200)

問題文を見ます PHP AND md5という検索クエリを脳に投げるとあんぐすとろむCTFでsei0oプロが解いていた問題を思い出したのでWrite-up

scrapbox.io

を見ます ただし問題が全く異なっていたので関係なくて悲しいな〜と思いながら眺めていると "cryptoをちょくちょく学んでいたためか伸長攻撃かな?と一瞬思ったが、そもそもハッシュが与えられないのでそれは違った。" という1文を見つけます これをチームメンバーに投げて少し考えて2時間ほど寝るとチームメンバーが解きます

感想

  • 問題が基本的に簡単なのが100%原因ではあるけどプロのチームと同じ順位ですごく嬉しい
  • 解いている問題が被る(特にkeymoon 死んで詫びるつもりは無いけどごめんなさい)ことはあったけどいい感じに協力しつつ解けてよかった 特にぼくのWrite-upの最後の4問は本当にチームワークだった(f31337は違うけど)
  • PPCをえかすどがすぐ倒してzoheがRevを一人で倒してkeymoonもSignature以外は一人で回答数が少ない問題を倒しまくっててこわかった チームを追い出されないように頑張りたい
  • sei0oを崇めよ

SuperCon予選

やったこと

  • 眺めているとすごく難しそう

  • まあ考えていると(0, 0)からDFSではいができることに気づく

  • 実装する

  • 自明に「今までの距離 + 現在地から原点までの最短距離 > n」が成り立つ時はアなので枝刈り

  • DFSはstackを使ったほうが速いらしい

  • これは罠で実装が出来なかった

  • inlineをつけると1.5倍ぐらい早くなった

  • 原点の4近傍の点が障害物もしくはもう通った点で埋まっていれば戻ってこれないので枝刈り

  • ここらへんで高速化のアイデアが尽きる

  • 少し考えると答えがm<= 2ぐらいまでなら埋め込めることに気づく

  • さらに考えると原点からの距離がn / 2以上の障害物は関係ないのでmの値を減らせる

  • 答えを埋め込む

  • ここでバグりまくる

  • 最終的に添え字の"[]"の位置が間違っていることに気づく

  • とりあえず m = 0, 1(0というのは答えに関係のある障害物が無い時)の答えを埋め込む

  • m = 2の時を埋め込む

  • 死ぬほどつらいのでn = 20, m= 2の時に絞る

  • i7-6700が頑張る

  • 3,40分で答えが出る

  • 埋め込む

  • 応募用紙を書く

  • 提出する

テストケースの実行結果

Ans= 133662, time=19

ソースコード

#include "sc1.h"
#include "stdio.h"
#include <iostream>

using namespace std;

int dx[4] = {0,1,0,-1};
int dy[4] = {-1,0,1,0};

int ans = 0, cnt = 0, cnt1;
int index2[20];
bool mas[100][100], flag[100][100];
const int offset = 50;

int index[21][21] = // 2*10^6 以上あるので省略

inline void dfs(int x, int y, int l){
    if(mas[51][50] && mas[49][50] && mas[50][49] && mas[50][51]) return;
    if(scN - l < abs(offset - x) + abs(offset - y)) return;
    mas[x][y] = true;
    for(int i = 0; i < 4; ++i){
        int nx = x + dx[i];
        int ny = y + dy[i];
        if(!mas[nx][ny]) dfs(nx, ny, l + 1);
        if(l + 1 == scN && nx == 0 + offset && ny == 0 + offset) ++ans;
    }
    mas[x][y] = false;
}

int main(){
    scInput();
    for(int i = 0; i < 100; ++i) for(int j = 0; j < 100; ++j){
        mas[i][j] = false;
    }
    for(int i = 0; i < scM; ++i){
        if(abs(scB[0][i]) + abs(scB[1][i]) > scN / 2) continue;
        mas[scB[0][i] + offset][scB[1][i] + offset] = true;
        cnt1 = i;
        index2[cnt] = i;
        ++cnt;
    }
    if(cnt == 0){
        scOutput(ans0[scN / 2 - 7]);
        return 0;
    }
    if(cnt == 1){
        scOutput(ans1[scN / 2 - 7][index[scB[0][cnt1] + 10][scB[1][cnt1] + 10]]);
        return 0;
    }
    if(cnt == 2 && scN == 20){
        int ax = scB[0][index2[0]], ay = scB[1][index2[0]];
        int bx = scB[0][index2[1]], by = scB[1][index2[1]];
        int a = index[ax + 10][ay + 10], b = index[bx + 10][by + 10];
        if(a > b) swap(a, b);
        scOutput(ans20_2[a][b - a - 1]);
        return 0;
    }
    mas[offset][offset] = true;
    dfs(0 + offset, 0 + offset, 0);
    scOutput(ans);
}

感想

正直定数倍高速化コンテスト感が強かった

チーム名はaldas(リパライン語で雷雲の意)です かっこいいね(@sashimiwiki さんに付けて頂きました ありがとうございます!)

JOI2017/2018 本選

今更ですが書いておきます

忙しくても反省は見たほうがいいかも知れません

1日目

秋葉原に(多分最速で)到着します

秋葉原

 ウニを1人でしました

 激エモスポットに行きました

 みんなでごはんを食べました

 この後出発します

筑波

お財布を電車に忘れていることに気づきます

practiceに遅刻します(一緒にいた人々と委員会の方々にご迷惑をおかけしました)(申し訳ありません)

もう一度筑波に着きます 走ります

 立食をします

 

独房で人々(@yfba_ @Kutimoti_T @lumc_ @57tggx)とお話をします

寝ます(28:00)

2日目

起床をします(5:30)

 ごはんを食べて会場に行きます

 

 帰ります

↑結構つらかった

 

感想

楽しかった 税金でオフ会が出来るのはやっぱり素晴らしい

反省

財布を忘れてPractice Session に遅れるのはやめましょう

ちゃんと前日は寝ましょう

独房の鍵は返しましょう

部分点は自明でも実装したほうがいいです なにか見えることがあるはずです

何が言いたいか

www.pixiv.net

を見ましょう

セキュリティ・キャンプ2018 応募用紙

問1

問題文

今の(コンピューター、マルウェア、メモリ、etc.)フォレンジックスにはどんな課題があるでしょうか?

解答

大まかに技術的な問題とそうでない問題に分けて書きます。
まず技術的な問題を書きます。
メモリフォレンジックスは揮発性のあるRAMのデータを解析する必要があります。
犯罪が実行された時付近のメモリの中身をダンプしておく必要があるので、データのバックアップに使う容量が大きくなってしまうという難点があります。
マルウェアフォレンジックスでは静的もしくは動的に解析する必要があります。
静的解析の場合、難読化されたアセンブリを人間が読むことが難しく、難読化されていなくても解析するためには深く広い知識が要求されるという課題があります。
動的解析においては、マルウェアが仮想環境であることやその他の環境の情報を検知して実行できなくしたり、特定の個人や企業・組織が目的である場合、マルウェアがあくまでもダウンローダとしての機能しか有さず、ダウンローダ以外の部分をサーバからダウンロードする時にサーバがその特定の個人や企業でないことを検知して解析させないように設計されている場合があります。要するに実際にマルウェアの標的となる環境を再現することが難しいという課題があります。
ネットワークフォレンジックスにおいては、リアルタイムにパケットを収集しすべて解析する必要があるので今まで上げたフォレンジックスの手法の中で一番設備の導入にお金がかかります。またネットワーク技術の進歩につれてトラフィックの量も増えるので、より高速にパケットをキャプチャして解析しなくてはならないという課題もあると思います。
法的な問題として「証拠として成立させるのが難しい」・「日本ではまだ広く普及しているとはいえない」ということが挙げられます。
前者については仮に犯罪が確認出来るようなデータを収集することが出来たとしてもその裁判官に真正性が認められるのが難しいということです。
これを解決するためにはデータを複製・移動する時に毎回作業ログやデジタル署名を残し、書き込み防止機能で改ざんや破壊を予防したりして真正性を示す必要があります。
この煩雑さはデジタルフォレンジックスの1つの課題だと思います。
後者については日本での法整備が追いついていないのとデジタルフォレンジックスが注目される機会が他のセキュリティ関連の事と比べて少ないためからか実際に活用している企業が45%ほどに留まっています。(https://digitalforensic.jp/wp-content/uploads/2017/05/df-chosa-13.pdf)
アメリカは特にデジタルフォレンジックスが日本より普及していて、その背景には2006年に出来たe-Discoveryがあります。
国が法整備をしたり技術を認知させる事が重要だと思います。

問2

問題文

匿名化通信を利用することによって犯罪者が特定しにくくなっています。匿名化通信の利用を認めつつ、犯罪者を特定するにはどうしたらよいですか?

解答

金銭を得ることが目的の犯罪ならば必ず入出金が伴うのでそこから犯人を辿ることができる可能性はありますが、そうでない場合は完全な匿名化通信において犯人が自分で自分であることを証明してしまうようなミスを犯さない限り無理だと思います。
そこで1つの落とし所としてプロパイダ、もしくは政府などは匿名化を解除できるような1段階弱い匿名化通信が挙げられます。
ただ、この能力が濫用されると憲法の通信の秘密に確実に違反してしまうと思います。
そこでどうしても犯罪者を特定するためには匿名化通信の解除(=利用者の特定)が必要な場合、警察がよく令状を裁判所に請求するように、捜査機関は毎度裁判所に匿名化通信の解除を請求するような制度があるといいと思います。
これなら権力者がこの制度を悪用することも出来なくなると思います。

問3

問題文

管理しているネットワーク内にウイルスに感染した端末が存在するかどうか、そしてその端末はどれなのか、あなたならどうやって突き止めますか?

解答

ネットワークの端末に(その端末で使えるウイルス対策ソフトが存在するならば)ウイルス対策ソフトをインストールしておきます。これでスキャンをして、もしウイルスがスキャンされたならその端末はウイルスに感染していると判断します。
これで見つからなかったらネットワークのキャプチャをして、得られたパケットの解析をします。ネットワークの外部と通信をするウイルスには「定期的な通信・パケットのサイズが小さい(もしくは一定)・成立しない(=SYNパケットだけ)TCP通信」などの特徴があるので、そのようなパケットが無いかを調べます(matplotlibなどでグラフにすると分かりやすいと思います)。もしそのようなパケットがあれば、そのパケットを送受信している端末がウイルスに感染していると判断します。
ここまでで見つからなければとりあえず現在は外部からのウイルスの感染は無いと判断します。

問4

問題文

あなたは、とある取引先のシステムを保守契約に基づいて保守しています。そして、取引先のシステムに重大な脆弱性があることが分かりました。このまま放置すると、顧客情報の流出など、甚大な被害が想定されます。しかし、その旨を取引先に説明しても、「1万円でなんとかしろ」と無理を言われます。あなたは、相手に対する説明責任は果たしているものとし、こちらに法的な非はない状態とします。しかし、何かあったときに被害をうけるのは、先方だけではなく、多くのシステムの利用者も含まれます。あなたなら、どうしますか

解答

結論からあげると私は1万円(人件費)で出来る範囲のことを説明した上でするべきだと思います。
もちろん取引先のシステムの利用者が被害を被ることはあってはならないと思いますが、企業は仕事に見合った代金を支払うべきだと思います。
仮に1万円で何とかした場合、次に重大な脆弱性が見つかった時も1万円(もしくはそれ以下)で何とかしろと言われることが想定されます。
また、なぜ1万円分の仕事を説明した上でするべきだと思うかというと、実際に1万円でどれぐらいの仕事を(こちら側が)するのかを知ってもらうべきだと思うからです。
そうすれば仕事をする前は理解してくれなくても「1万円では安すぎる」ということを分かってもらえると思います。
あとこれは個人的な感情ですが、システムの利用者も重大な脆弱性を1万円で修正させようとする会社のサービスを利用して欲しくないし、するべきでは無いと思います。なのでもし脆弱性を突かれて個人情報等が流出したとしてもいいのではとも思います。

感想

  • 恥ずかしい
  • 日本語が難しい
  • これでも過去の応募用紙の100倍は書いたので通ってほしい
  • HackMD最高

追記

通ってました 参加者の方々と講師の方々、よろしくお願いします

JOI2017/2018 予選参加記

結果

ABCEの4完(400点)でした うれしいね
コードを晒します

A 鉛筆 (Pencils)

int main() {
	int n, a, b, c, d;
	cin >> n >> a >> b >> c >> d;
	int ap = n / a;
	int cp = n / c;
	if (n % a != 0) {
		ap++;
	}
	if (n % c != 0) {
		cp++;
	}
	if (b* ap > d * cp) {
		cout << d* cp << endl;
	}
	else {
		cout << b * ap << endl;
	}
}

はい

B 双六 (Sugoroku)

int main() {
	int n, a, ans = 0, cnt = 0;
	cin >> n;
	REP(i, n) {
		cin >> a;
		if (a == 1) {
			cnt++;
		} else {
			ans = max(ans, cnt);
			cnt = 0;
		}
	}
	ans = max(ans, cnt);
	ans++;
	cout << ans << endl;
}

Aより実装楽で面白いね

C 幹線道路 (Trunk Road)

int main() {
	int h, w;
	int num[25][25];
	cin >> h >> w;
	REP(i, h) {
		REP(j, w) {
			cin >> num[i][j];
		}
	}
	int ans = INF;
	REP(a, h) {
		REP(b, w) {
			int tmp = 0;
			REP(i, h) {
				REP(j, w) {
					tmp += min(abs(i - a), abs(j - b)) * num[i][j];
				}
			}
			ans = min(ans, tmp);
		}
	}
	cout << ans << endl;
}

一瞬迷ったけど普通に全探索

D 水ようかん (Mizuyokan)

わけがわからないよ
bit全探索を書こうとするも書けないので0点

E 森林伐採(Deforestation)

int dx4[4] = {0,1,0,-1}, dy4[4] = {-1,0,1,0};
 
int main() {
	ll dp[31][31];
	dp[0][0] = 0;
	ll a[31][31];
	ll dist[31][31];
	ll h, w;
	cin >> h >> w;
	REP(i, 31) {
		REP(j, 31) {
			dist[i][j] = -1;
		}
	}
	dist[0][0] = 0;
	REP(i, h) {
		REP(j, w) {
			cin >> a[i][j];
		}
	}
	queue<P> que;
	que.push(P(0, 0));
	while (!que.empty()) {
		auto p = que.front(); que.pop();
		int di = p.first; int dj = p.second;
		REP(i, 4) {
			int nx = di + dx4[i]; int ny = dj + dy4[i];
			if (nx < 0 || ny < 0 || nx >= h || ny >= w) continue;
            ll COST = dp[di][dj] + (dist[di][dj] * 2 + 1) * a[nx][ny];
			if (dp[nx][ny] == COST && dist[nx][ny] > dist[di][dj] + 1) {
				dist[nx][ny] = min(dist[nx][ny], dist[di][dj] + 1);
				que.push(P(nx, ny));
			}else if (dist[nx][ny] == -1 || dp[nx][ny] > COST) {
				dp[nx][ny] = COST;
				dist[nx][ny] = dist[di][dj] + 1;
				que.push(P(nx, ny));
			}
		}
	}
	cout << dp[h - 1][w - 1] << endl;
}

拡張Dijkstraっていう分類の問題らしい
BFSをして特定のマスまでのコストとそのときの(0, 0)からの距離を持つだけ
なんでAC出来たんでしょうか 僕にも分かりません

追記

上のコードが限界だったのでちょっと手直ししました(2018/11/26)

int dx4[4] = {0,1,0,-1}, dy4[4] = {-1,0,1,0};
 
int main() {
    // 入力
    ll dp[31][31], a[31][31], dist[31][31];
    ll h, w;
    cin >> h >> w;
    REP(i, 31) {
        REP(j, 31) {
            dist[i][j] = -1;
        }
    }
    REP(i, h) {
        REP(j, w) {
            cin >> a[i][j];
        }
    }

    dist[0][0] = 0; // (0, 0)までの距離は0
    dp[0][0] = 0; // (0, 0)までのコストは0
    queue<P> que;
    que.push(P(0, 0)); // (0, 0)からスタート

    while (!que.empty()) {
        auto p = que.front(); que.pop();
        int x = p.first; int y = p.second; // もともといたマス

        REP(i, 4) {
            int nx = x + dx4[i]; int ny = y + dy4[i]; //四近傍で回す
            if (nx < 0 || ny < 0 || nx >= h || ny >= w) continue; // 領域内か判定
            ll cost = dp[x][y] + (dist[x][y] * 2 + 1) * a[nx][ny]; // https://twitter.com/6Lgug/status/1066718346509348864

            // 行っていないマスもしくは行っているけど現在のコストのほうが少ないなら更新
            if (dist[nx][ny] == -1 || dp[nx][ny] > cost) {
                dp[nx][ny] = cost;
                dist[nx][ny] = dist[x][y] + 1; // 距離は前のマスまでの距離+1なので
                que.push(P(nx, ny));
                continue;
            }

            // コストが同じでも距離が短いなら更新((nx, ny)以降のマスまでの最小コストが減りうるので)
            if(dp[nx][ny] == cost && dist[nx][ny] > dist[x][y] + 1) {
                dist[nx][ny] = dist[x][y] + 1;
                que.push(P(nx, ny));
            }
        }
    }
    cout << dp[h - 1][w - 1] << endl; // 出力
}

F L番目のK番目の数 (LthKthNumber)

全探索しようとしましたが書けませんでした

感想

3完+部分点しようと思っていたので嬉しい
が部分点も取れない実装力の無さには萎えました
(本戦行けるといいなあ...)

追記

本選行きます
0完太陽☼にならないようにがんばるぞい

プログラミングコンテスト攻略のためのアルゴリズムとデータ構造 を買いました

ひどい写真ですね。