D言語の競技プログラミング用ライブラリを作ってみた

タイトルに反して実は作り始めたのは1.5年ぐらい前ですが…

結構安定してきたんで紹介をします。でも使うのはオススメできないかも(いろいろ壊れてるかもしれないため)。要するにせっかく作ったから見てくれというやつです。

準備

  • まずD言語コンパイラをインストールします
  • D言語のパッケージ管理システムdubをインストールします(https://code.dlang.org/)

  • お好きなところにA.dを作って,sample.dをコピペします(このリポジトリをcloneする必要はないです)

  • dub build --single A.dと打ちます

  • ./Aができない→どうしてこんなことに…
  • ./Aができた→こちらを想定します

sample.dは以下のようになっているはずです

/+ dub.sdl:
    name "A"
    dependency "dunkelheit" version="1.0.0"
+/

import std.stdio, std.algorithm, std.range, std.conv;
import dkh.foundation, dkh.scanner;

int main() {
    Scanner sc = new Scanner(stdin);
    scope(exit) assert(!sc.hasNext);
    int a, b;
    int[] c;
    sc.read(a, b, c);
    writeln(a, b, c);
    return 0;
}

このコードは,整数2個と配列1個を入力して出力するコードです。例えば'./A'を実行して

1 2
3 4 5

と入力すれば,

12[3, 4, 5]

と出力されるはずです

dependency "dunkelheit" version="1.0.0"でversion1.0.0を指定しています。もし今後後方互換性壊したりしても,とりあえずv1.0.0を指定しておけばv1.0.0が動く!すごい!

ソースコード結合

提出するときにはソースコードを1つにする必要があります。ここで

dub run dunkelheit:combine -- -i=A.d -o=A_submit.d -c -u

と打つと,A_submit.dが出来ます。これをそのまま提出すれば動くはずです。多分。

ドキュメント

ライブラリにはドキュメントがついています(ついていない部分もあります)。 https://yosupo06.github.io/dunkelheit/で見ることが出来ます。

機能紹介

いくつか機能を紹介します。

Scanner(dkh.scanner)

一番使います。

サンプルのように読み込みたいものを全部引数に入れてsc.read()を呼べばよいです。

scope(exit)はこのスコープ(= int main)を抜けたときに実行するという意味で,sc.hasNextは全部読み込み終わったかを返すので,要するにscope(exit) assert(!sc.hasNext)はmain関数を抜けたときに全部読み込み終わってるかチェックしてくれます。

また,配列型は,「次のトークンから行が変わるまで全部読み込む」をやるので,

N M
a_1, a_2, ..., a_N

みたいなのを読み込みたいときに便利です(sc.read(N, M, a))。

N M
a_1 b_1
a_2 b_2
:
a_N b_N

みたいなのはどうしようもないです。

Segtree(dkh.container.segtree)

やっぱりライブラリと言ったらsegtree, segtreeと言ったらライブラリみたいなところがあるよね。

D言語はラムダを短くかけるので,

auto seg = LazySeg!(int, int, (a, b) => max(a, b), (a, b) => a+b, (a, b) => a+b, 0, 0)(n); でstarry sky treeが出来ます。

auto seg = LazySeg!(int, int, max, "a+b", "a+b", 0, 0)(n) とも書けます。

dkh.datastructure.segtree のexamplesに詳しく書いています。seg[0..3].sum()区間取得とかseg[0..2] += 10区間加算とかお気に入り(自画自賛)

modint(dkh.modint)

scannerの次ぐらいに使います。自動mod取り構造体とも呼ばれます。

alias Mint = ModInt!(10^^9 + 7)

で,

Mint a = Mint(10);
Mint b = Mint(11);
writeln(a+b); //21
writeln(a-b); //1000000006
writeln(a*b); // 110
writeln(a/b); // 逆元も
writeln(a^^b); // べき乗も!

のように使えます。また,dkh.numeric.convolutionをimportすればfftも使えます

Mint[] a, b;
writeln(multiply(a, b));

とか

CI

D言語にはunittestを書きやすい支援機能がついているので,使いました。 githubにpushするたびにdroneが走って,いろんなバージョンでチェックしてくれます。

f:id:yosupo:20180529021956p:plain

↑こんな感じ

なので一度潰したバクは(理論上)再発しないですね!安心!

なお,CIにはhttps://github.com/drone/droneを使ってます,便利

CF Avito Code Challenge 2018

AB

はい

C

ちょっとすき,条件がつよいから結構Noが多そう
→サンプルの図を見ると次数>=3が2個以上がたいへんあやしく

D

ちょっとすき,値が大きい + 2進数を感じさせる演算 + maximize
→これAGCで見た
 →上bitから貪欲するとDPです
  →250(は?)(きらい(手のひら返し))

E

すき →クエリを選んで,全体maxをxにする?
 →まずどれかの要素がxにならないといけない
  →よく考えるとどれかの要素がxにできたら全体maxもxにできる
   →全体maxと言いつつ,要素ごとにほとんど独立の問題

要素ごとに独立に考えられる
→90度回転待ったなし
 →戻したい雰囲気がある…あるね?
  →bool DPで戻したいときはmodにするのが鉄板で

G

問題を想像する
→かなりならしを感じる
 →ならしたい…ならしたくない?ならせるね

ならせる
→ここから方針を間違えてしまい…

F

円環はダメ
→列に展開したい…
 → bを3週させて(b[i]-L, b[i], b[i]+L)考えると…?

自由度が一見高いように見えて,さすがにminimizeしてることとか使うと自由度が下がるはず
→どうやらbをrotateしてa[i]-b[i]みたいな感じで結ぶのしか考えなくて良くなる
 →O(N2)

この2個+二分探索をまとめると?
→ウニウニいい感じに動いていきます,完全マッチング作れますか,マッチングは一個ずつ進んでいくパターンしか考えなくていい
 →a[0]と結ぶやつを全部試して,同時並行シミュレーション?
  →できそう
   →本当に通るかな?

ARC 098

いつもはvmware+linux+vscodeで出ていたんだけど,今回はwindows+wsl+clionで参加(したかった)

F

お金を配りながら移動していく,ただし移動するときには頂点ごとにしきい値がある
→サンプル1を書いてみて想像をする
 →どうやらお金はありすぎて困ることはない
  →二分探索?

二分探索できるのでとりあえず二分探索してみる(logは定数なため)
→最初のお金Zが決まるから可能か?を求める
 →お金を配ると結局どうなる?
  →移動可能範囲が狭まっていく
   →移動可能な範囲が狭まっていくなか,うまく全部の頂点を辿れるか?
    →ものがstrictに減っていく時は逆から考えると良い(一般に減らしていくより増やしていくほうが簡単なため)

ゴールから考えるとどうなる?
→最初のお金が決まっているため最後のお金も当然決まる
 →お金を頂点から回収していって,だんだん移動可能範囲が増えていく
  →どうやら多項式にはなった(TLEだけど)

どうやらゴール位置が重要っぽい
→例えばゴール位置を決め打つと簡単に計算できるか?
 →priority_queueとか使って探索していくとうまくやると行けそう
  →じゃあどうやって複数のゴール位置についてやる?
   →ゴール位置が複数あると,複数位置からじわじわ探索が広がっていって,衝突したらmergeみたいな雰囲気がありそう
    →データ構造をマージする一般的なテク?

制約もそれっぽいし,解法はデータ構造をマージする一般的なテクだろうと想像がつく
→log 3個?
 →流石にデマパートはlog 2個からは落とせなそう(なんか隣のやつをsortして持っておくみたいな操作が必要そう),じゃあ二分探索やめるしかない?
  →二分探索やめるか

二分探索をやめる
→最初に持っている額を決め打てない
 →最初は0円スタートで,足りなくなるたびに適時無からお金を生成する感じで

まとめる
→ゴールから考える
→最初は0円スタート
→それぞれの頂点について,「もしこの頂点がゴールだったら?」を並列にシミュレーションする感じ
 →シミュレーションそれぞれについて,できる限り進めて,進まなくなったら「無からお金をいくら生成すればシミュレーション進みます」を計算してpriority_queueに突っ込む,それが最小の額だけ無からお金を生成して,シミュレーションを進める
  →これを繰り返せばなんとかなりそう,色々大変だけど計算量もなんとかなる?
   →AC

C

累積和流行ってる?

D

0いるか?僕はいらないと思います

E

えー全然わからない,おわりです →よく考える
 →最大と最小の両方に自由度があるのなに?
  →よくみるとN=2000
   →最大か最小どっちか固定して,それぞれO(N)に違いない,自由度1ならなんとかなるだろ

どっちを固定したい?
→最大固定はよくわからないけど,最小固定はうれしさがある(部分列にいくつかの要素を含められないという条件になるため)

最小を固定してみる
→いくつかの要素が取れなくなる
 →要するに数列が分解される
  →いくつかの数列が与えられる,取り除いたやつの最大値を最小化

可能かどうかの判定は?
→数列の要素数から簡単にわかる

最大値の最小化は?
→そもそも最小値は絶対取り除けるんだから,最大値の最小化もクソもなくないか?
 →[1], [100, 100, 100, 100]とかになると1を取れなくなるのか
  →連結成分ごとに取り除ける個数が決まっていて,そのなかではかなり自由?(APC Dを思い出すなあ(唐突な宣伝))
   →正しそう

AGC 024

E

解けません,全然解けません,おわりです

D

問題の理解が大変

とりあえず最小値を求めないといけない
→AGCで無向木の意味不明な性質を求められたらとりあえず直径に注目しろという法則があり
 →(直径+1)/2色は絶対必要なことがわかる
  →なので(直径+1)/2色で塗り分けられることがわかる(理由: AGC)

最小値は求まったので(求まったとは?)葉の個数を数えればいい
→よくわからないよくわからない
 →サンプルを実際に書いて眺める
  →なんか深さiはa_i分木みたいなのを作る?
   →ちょうど答えがいい感じのサイズ(250)になったので,正しい(強気)

F

読解が大変
→高速ゼータ変換部分列版をしろという問題
 →高速ゼータ変換を一般化した論文があるんですが,それだとうまくいかない
  →そもそも何がしたいのか?
   →文字列が1個あったとき,部分列を1回ずつ列挙する方法は?(ここで経由する状態がシンプルだといい)
    →普通に先頭から0か1をとっていけばいいですね
     →AC

A

A問題なのにサンプルの出力にUnfairが無いため,Unfairを出力することはない

とりあえず適当に手で試す
→ああ-1倍されるのか
 →AC

B

AtCoderでこれと似たような問題で,残すやつに注目するとLISという問題があったはず
→残すやつに注目すると?
 →ああ単調増加
  →AC

C

二次元平面(X: i, Y: a_i)を想像する
→階段がはやせる,階段しかはやせない
 →階段をはやして数列を作れるか?
  →a[i]より2以上a[i+1]の方が大きかったらダメ,あとa_1 != 0でもダメ(こっちはサンプルからわかる)
   →これを満たしていれば作れる,作り方も自然なので数えるのは簡単
    →AC

GCJ R2

思考要素が無さすぎて書くことが…

D

読めないね

読めた?(start gridは任意に選べると誤読をしていた)
→16パターン全探索するだけだと思うんですが…
→WA

間違いが見つからない&問題が読めないので誤読を疑う
→誤読だね

→WA
→1*nで死んでいて
→プログラムがバグっていて
→AC

C

行と列で被らないように←ここあたりで二部グラフに変換してからのフローっぽいなと思う
→フローでした

A

貪欲だけどどうやって実装しよう,適当に実装しよう
→AC

B

被らないようにペア(r, b)をとっていく
→最小のサイズの凸包を作る問題のアルゴリズムとかなり似ている
→(r+b)が小さい順に貪欲?
→反例生成

貪欲じゃなさそう?DP
→最終的な人数を決め打って,Rを全部振り分けてからBを全部振り分けると考えるとなんかDPになった
→AC

Yukicoder #191

D

えーN<=32, 1e9, 構築,これは明らかにbase2で考えるタイプの性質であり
→base2で考えると解けました

E

誤差が非常に厳しい
→微調整をしたいね
 →sqrt(x+1) - sqrt(x)は?だいたい1/sqrt(x)
  →全然精度足りないね
   →じゃあ(sqrt(x+2)+sqrt(x)) - 2×sqrt(x+1)は?
    →だいたい1/xsqrt(x),精度足りそうだね

大雑把に作って,[x, x] -> [x+1, x-1]という遷移で誤差を微妙に調整する事を考える
→微妙に精度足りないね

とりあえず大きいほど微調整が聞くので,1e-10, 2e-10, 4e-10の微調整が可能なやつを用意しておいて,それで最後の調整をすることにする
→うまくいくときもあるしうまくいかないときもあるね
 →じゃあ何回かやればいいか!w
  →TLE
   →微妙に定数倍改善
    →AC

A

うんうん

B

K=0いるか?

C

素数の性質は難しいため,あんまり素数の性質には頼らない解法をしたい

構築の解き方①であるx進数は流石に無理そうなので, 構築の解き方②である「だいたいKをつくる→だいたいさっきのあまりをつくる→だいたいさっきのあまりをつくる→…」を考える

a+bが素数のペアが作れれば1×1, 1×2, ..., 100×100で誤差99まで作れる
c+dが素数のペアが作れれば1×1, 1×2, ..., 10×10で誤差9まで作れる
e+fが素数のペアが作れれば1×1, 1×2, ..., 1×10で誤差0になる

ので要するに,a+b, c+d, e+fが素数でそれ以外のペアは素数ではない(a, b, c, d, e, f)があればいい
→きっと適当に乱択で出てくるだろう
 →出てきた
  →WA
   →えー生成コードくんが間違っており
    →AC

CF #483

A

素因数系?1018なのでgcd/lcmでなんかやるタイプだろう

条件は?
→とりあえずp, qは既約にする
→gcd/lcm系なので素因数ベクトルで考える
→まずb=10の場合は?
 →どうやらqが2x * 5yならば可能
  →さすがにqの素因数のなかでbに含まれないものがあったら不可能だろう
  →これの逆(bに含まれないものがなかったら可能)は言える,多分必要十分

整理する
→p, qを既約にする
→qの素因数のなかでbに含まれないものがあるか判定

これをgcd / lcmで実現可能か?
→p /= gcd(p, q)をたくさんやれば共通素因数が全部除去できるので,可能
 →Pretest Passed?
  →TLE
   →小手先改善,はたして

B

fの性質は?
→CSAで最近見た,経路数 mod 2に帰着できるのでリュカの定理

これのmax?一体全体
→…

N = 5000とは?O(N polylog)ではなくO(N2)ということ?
→経路数,O(N2)と来たらどう考えてもDP[x][y] = DP[x-1][y] + DP[x][y-1]が怪しい
→あーこの形でDPがうまくいくんですね

→Pretest Passed

C

問題文が長い,パス

D

かなり実家を感じる

えー長方形を塗って上書きしていく
→長方形iは長方形i+1, i+2, ...の影響を受ける
→後ろのやつらの影響を受けるが前のやつらの影響は受けない,じゃあ逆から考えれば良い性質?

整理する →以下のクエリを処理せよ
 →長方形を塗る
 →長方形のなかで,まだ塗られていない場所はあるか?
→これRUPCで見た
→…
→これRUPCでは解けなかった

思考を変える,クエリが全部オフラインで二次元平面なので,平面走査もアプローチとして有力だろう

平面走査をするとどういうクエリになる?
→二次元平面に横線を足す
→二次元平面に横線を抜く
→二次元平面を上から見たときに見れる横線の一覧は?

3つめのクエリが絶望的,本当にこれを処理するの?
→よく考えると各横線について一瞬でもわかるかがわかればいい
→じゃあ1個取得にして取得O(polylog)でならしO(N polylog)のタイプに違いない

3つめのクエリは変更可能
→二次元平面を上から見たときに見れる横線のうち,今まで見れていない奴は1個でもあるか?

とりあえずここまでくれば可能か?
→データ構造なので,とりあえず平方分割で可能か考える
 →可能そう
  →じゃあSegTreeは?
   →多分可能

→実装
→バグって大変でした…(ストレステストまで書いた)
→Pretest Passed,はたして?
→Pretest Passed数みたら大間違いでした。かなしいね