えびちゃんの日記

えびちゃん(競プロ)の日記です。

非再帰セグ木上の任意始点にぶたん

セグ木は知っているとします。 知らない人は えびちゃんのスライド なり適当な記事なりを読んでください。

セグ木上のにぶたんも上のスライドに載っていますが、もう少し解説したいかなと思います。 上のスライドにある図を見るとイメージが湧きやすいかもしれません。

愚直にやると、セグ木の区間*1取得と、二分探索で、ふたつ log がついてしまいます。 セグ木の実装を非再帰でやって、\([l, r)\) の区間和を \(O(\log(r- l))\) でできるとしても、オーダーは落ちません。

参考までに、\(n = 10^6\) とすると、\(n\log(n)^2\) は \(n\sqrt{n}/2.5\) 程度ですが、\(n\log(n)\) は \(n\sqrt{n}/50\) 程度です。 落とせる log は落としておきたい気がします。

この方針以外の実装もある気がしますが、えびちゃん的にはこれが好きなので、とりあえずこれを紹介します。 気づいたらそのうち宗派が変わっているかもしれません。

定義

セグ木の葉の個数を \(n\) とします。 セグ木の元の配列長が \(n\) ということです。 区間 \([l, r)\) の和を \(a[l, r)\) と書くことにします。

左端を固定するとき

以下を満たす述語 \(p\) が与えられるとします。

  • 単位元 \(e\) に対して \(p(e)\) が true
  • ある \(i\) が存在して \(p(a[l, i))\) が false であれば、任意の \(i \lt j \le n\) に対して \(p(a[l, j))\) が false

このとき、\(p(a[l, i))\) が true かつ、次のいずれかを満たす \(i\) を返します。

  • \(i = n\)
  • \(i < n\) かつ \(p(a[l, i+1))\) が false

右端を固定するとき

以下を満たす述語 \(p\) が与えられるとします。

  • 単位元 \(e\) に対して \(p(e)\) が true
  • ある \(i\) が存在して \(p(a[i, r))\) が false であれば、任意の \(0 \le j \lt i\) に対して \(p(a[j, r))\) が false

このとき、\(p(a[i, r))\) が true かつ、次のいずれかを満たす \(i\) を返します。

  • \(i = 0\)
  • \(i \gt 0\) かつ \(p(a[i-1, r))\) が false

補足

一度 false になると、区間を広げても false であり続ける述語を受け取ります。 条件を満たす最大の区間を求めているのに相当します。 両方の場合において、引数と返り値を昇順に \(l, r\) とすると、\(p(a[l, r))\) は true となります。

\(p(e)\) が true であることを要求しているのは、そうしないと返り値がつらくなるためです。 -1 を返すのはあまりうれしくないし、そもそも単位元の時点で false なら、呼び出す前から答えがわかりますし。

なお、これは ACL のものの定義と同じです。

簡単な場合

まずは、セグ木を完全二分木に限定し、二分探索の始点を左端に固定した場合を考えます。 全区間の和を述語に渡した場合は false になるとします(そうかどうかの判定は述語を高々一回呼べばわかる)。

これは、必要な情報を管理しながら、ノードを下っていくことでできます。 必要な情報というのは、「今まで足されている値」「今見ている添字」くらいだと思います。

適当に非再帰で簡単に実装できます。 たとえば、左に潜るなら i <<= 1 のようなことをすればよいです。

auto x = e;  // 今まで足した値。単位元で初期化
while (i < n) {
  i <<= 1;
  auto tmp = op(x, a[i]);
  if (p(tmp)) {
    // 左の子を足しても大丈夫
    x = tmp;  // 足した結果で上書き
    i += 1;
  } else {
    // 左の子を足すとだめ
    // 特に何もしなくていいね
  }
}
return i - n;

一般の場合(左端を固定)

セグ木を完全二分木に限定せず、二分探索の始点を特に限定しない場合を考えます。始点を \(l\) とします。

セグ木で \([l, n)\) の区間和を求める際に見るノードを、左から順に見ていきます。 このノードたちの添字は、非再帰セグ木のループを使うと簡単に求められます(順序には注意)。

必要に応じて自分で図を書いたり、上のスライドの図を見るといいかもしれません。

これらのノードを順に見ていきながら、「ここまで加えると条件を満たさなくなる」という境界を探します。 このノードは \(O(\log(n))\) 個なので、単に線形探索するだけで問題ありません(ここすき)。

さて、そのようなノードが見つかったとします*2。 重要なこととして、あるノードの子たちを見ると、完全二分木になっています。 このことから、上で示した簡単な場合に帰着されるので、\(O(\log(n))\) 時間でできます。

以上を合わせて、\(O(\log(n))\) 時間で答えを求められます。

一般の場合(右端を固定)

上記で左から見ていたところを、右から見るようにするだけです。 off-by-one とかには注意。

遅延セグ木の場合

必要に応じて伝播しながらやると、同じ感じでできます。

おわり

候補が \(O(\log(n))\) 個なので線形探索をしても \(O(\log(n))\) 時間で済むパートすき。

*1:適宜、和はモノイド積と読み替えてください。

*2:見つからなければ全区間が条件を満たします。

α(n) とお近づきになりたい人のための記事

↓ 勉強し直しました ↓

rsk0315.hatenablog.com


union-find の計算量解析で出てくる \(\alpha(n)\) というのがありますね。 Ackermann 関数の逆関数として知られるやつです。

競プロ er の多くは、次のような認識をしていると思います。

  • 計算量に \(\alpha(n)\) が出てくるなら union-find が関係する以外ないと思っている*1
  • なぜ union-find でそうなるかはよく知らない
  • どういう解析で \(\alpha(n)\) が出てくるのかわからない
  • \(\alpha(n)\) がどんな性質を持っているかもあまり知らない
  • 実質定数だと思っている

最近、少しお勉強する機会があったので、紹介してみます。 えびちゃんもまだちゃんと仲よしになったわけではないです*2

\(\star\) 演算子の導入

唐突ですが、\(\star\) 演算子というのを導入します。 関数 \(f(n)\) に対して、次のような漸化式で定義されます。 ただし、\(n > 1 \implies f(n) < n\) とします。

\[ f^\star(n) = \begin{cases} 0, & \text{if }n \le 1;\\ 1 + f^\star(f(n)), & \text{otherwise}. \end{cases} \]

DP に慣れている人は読みやすいと思いますが、「与えられた \(n\) に \(f\) を適用して \(1\) 以下にするために必要な、最小の適用回数」を返します。 例を挙げてみます。

\(f(n)\) \(f^\star(n)\)
\(n-1\) \(n-1\)
\(n-2\) \(n/2\)
\(n/2\) \(\log(n)\)
\(\sqrt{n}\) \(\log(\log(n))\)
\(\log(n)\) \(\log^\star(n)\)

使い道の例

分割統治を例に出します。

サイズ \(n\) の問題があり、これをサイズ \(f(n)\) の問題 \(n/f(n)\) 個に分け、その結果を高々 \(a\cdot n\) 時間で合わせるときの計算量 \(T(n)\) を考えてみましょう。

\[ \begin{aligned} T(n) \le \begin{cases} 0, & \text{if }n\le 1;\\ an + \frac{n}{f(n)}\cdot T(f(n)), & \text{otherwise}. \end{cases} \end{aligned} \]

これを順に展開していくと、次のようになります。

\[ \begin{aligned} T(n) &\le an + \frac{n}{f(n)}\cdot T(f(n)) \\ &\le an + \frac{n}{f(n)}\cdot \left(a\cdot f(n) + \frac{f(n)}{f(f(n))} \cdot T(f(f(n)))\right) \\ &\le an + an + \frac{n}{f(f(n))} \cdot T(f(f(n))) \\ &\le \dots \\ &\le an + \dots + an + \frac{n}{f^k(n)} \cdot T(f^k(n)) \end{aligned} \]

\(n\) に \(f\) を \(k\) 回適用すると \(1\) になるとします。 \(f^k(n) = 1\) のとき、\(T(f^k(n)) = 0\) となることに注意します。

このとき、\(k\) 個の \(an\) があるので、次のように書けることがわかります。

\[ T(n) \le an\cdot f^\star(n). \]

\(\alpha(n)\) の定義

さっきの表の一部を抜粋して再掲します。

\(f(n)\) \(f^\star(n)\)
\(n-2\) \(n/2\)
\(n/2\) \(\log(n)\)
\(\log(n)\) \(\log^\star(n)\)

ある \(f\) に複数回 \(\star\) を適用した場合の挙動が気になりますね。

そこで、\(\alpha_0(n) = n-2\) とし、\(\alpha_{k+1}(n) = \alpha_k^\star(n)\) と定義します。 念のため補足すると、\(n\) に \(\alpha_k\) を \(\alpha_{k+1}(n)\) 回適用すると初めて \(1\) 以下になるということです。

\[ \begin{aligned} \underbrace{\alpha_k(\alpha_k(\dots(n)\dots))}_{\alpha_{k+1}(n)-1\text{ times}} \gt 1,\\ \underbrace{\alpha_k(\alpha_k(\alpha_k(\dots(n)\dots)))}_{\alpha_{k+1}(n)\text{ times}} \le 1.\\ \end{aligned} \]

そして、\(\alpha(n)\) を次のように定義します*3。 \[ \alpha(n) = \min\{k: \alpha_k(n)\le 2\}. \]

\(\alpha_k(n)\) が \(2\) 以下になるような最小の \(k\) を \(\alpha(n)\) とします。 \(2\) である理由は、\(n\ge 4\) に対して \(\alpha_k(n)\) の最小値が \(2\) であるためです。

値の求め方がわかったことで、以前よりは \(\alpha(n)\) が身近に感じられてきませんか? イメージがつかなければ、手などで計算してみてください。

資料の紹介コーナー

書くのが大変になってきたので、資料の紹介コーナーに移ります。 英語のものばかりですが、英語自体は難しくないと思います。

わかりやすいスライド

  • 図がたくさんある
  • 静的な区間モノイド積が \(\langle O(n\alpha(n)), O(\alpha(n))\rangle\) で解ける話が書いてある
    • disjoint sparse table は \(\langle O(n\log(n)), O(1)\rangle\)
    • \(\langle O(n), O(\alpha(n))\rangle\) でできるらしい
  • union-find の計算量解析も載っている

各種性質などの証明

  • 上の定義とは少し異なる \(\alpha(n)\) が用いられている(後述)
  • \(\alpha(n) = o(\alpha_k(n))\) などが載っている

Weak Epsilon-Nets, Davenport–Schinzel Sequences, and Related Problems

  • 上のリンク先の記事を書いた人の D 論っぽい
  • 1 章と Appendix A に \(\alpha(n)\) の話がある
  • \(\star\) を用いて定義した \(\alpha(n)\) が Ackermann 関数の逆関数になるのは、帰納的に簡単に示せると書かれている
  • \(\alpha(n)\) の亜種(定義が若干異なる)はいくつかあるが、漸近的には同じであることを示す手法が書かれている
  • この辺よくわかってません

定義の異なるやつに関する質問

  • あとでよむ

おわり

ほぼ丸投げ記事ですが、興味を持つ人がいてくれたらうれしいかなと思います。

*1:競プロの文脈なら概ね正しいとは思います。

*2:まだちゃんって誰?ではないです。

*3:Ackermann 関数に直接触れていませんが、それについては後述します。

PAST #4 参加記

考えていたことなどを書きます。

tl; dr

受検前

有料期間中に受けるかどうか考えました。

前提として、既にエキスパートの認定証を持っているので、以下のような葛藤が生じます。

エキスパートを取れた場合:

  • 少しの間、承認欲求が満たせる
  • 数ヶ月ぶんエキスパートの有効期間が延びる

エキスパートを取れなかった場合:

  • それなりのお金を払って無を取得できる*1
  • 気持ちがめちゃくちゃになる

そもそも、値段が安くはないので、気軽には払えません。 お布施という意味では払う方がいい気もするんですけど。

というのがあって、悩んだのですが、無料になってからうけることにしました。

問題公開日

なかなか公開されません。

ツイートをするとすぐ公開されてびっくりしました。

URL をエスパーしてコンテストページでリロードして待っていたのですが、PAST 過去問 の一覧に反映されたのは問題公開より後だった気がします。

はじまるよ

一覧を見ると「構文解析」と書いてあったので、うれしくなりつつも「どうせ構文解析要素は薄いんだろうな」と思いました。

A

3 つの要素の中央値は「最大値と最小値でないもの」なので、以下のように求めました。

let min = a.min(b).min(c);
let max = a.max(b).max(c);
let med = min ^ max ^ a ^ b ^ c;

総和から引いてもいいと思います。

B

特になし。頭が無だったので "ERROR" ではなく "0" とか書いていました。

C

特になし。

D

特になし。

この辺で、作業用 BGM を準備しようと思って(ナメていませんか)、前によく見ていた動画を探そうとしたのですが、見つかりません。 少し調べると、投稿者が動画もアカウントも全て消していたことが判明し、かなり動揺しました。これを書いている今も引きずっています。

E

Rust は標準に順列を生成するのがないので、itertools を使いました。 そういうのがない言語だとどうするのがいいのでしょう。

ここでは長さ \(N \le 5\) の順列を全て生成すればいいので、「\(N\) 進数 \(N\) 桁を全部つくり、各桁が distinct なもののみ見る」とか? よく考えると、この問題では \(26^N\) 通り全部作って判定するのが楽かもしれません。

動揺のせいか、D のコードを投げて全ケース RE になっていました。 PAST #1 でも同じことをした記憶がありますね。

F

特になし。

G

動揺のせいか、「. が見つかったら . に変えて、判定を終えたら # にする」という謎を実装していました。

判定用に union-find を使っていたのですが、自作ライブラリ用の bundler がバグっており、その直しに数分を要しました(あーあ)。

H

特になし。

これ、どう線形で解くんでしょう。

I

尺取りでもいいと思うのですが、にぶたんでもいいと思ったのでにぶたんをしました。 円環は 2 周持つのがセオリーだと思いますが、変に配列外参照をしたりすると嫌なので、念のため 3 周持ちました。

この辺で「あれ? そういえば構文解析たのしみだった。何問題だったかにゃ」と思って、問題一覧を見ると既に AC しており、全てを察してかなり嫌な気分になりました。

J

超頂点を作って Dijkstra をします。

K

\(10^9\) で割った余りとあったので、\(10^9+7\) が MathJax の描画ミスとかで \(10^9\) に見えているのではないかとか疑い、右クリックして確かめました。

転倒数と書いてあるのでセグ木を貼るのかと思いましたが、値が \(20\) 以下とのことなので、そっちで抑えるんだろうなとなりました。

系列二つに対して、「その系列における転倒数」と「各値の出現回数」がわかれば、それらをくっつけた系列に対するそれらの値もわかります。結果として、全体の転倒数もわかります。

これは ACL practice L に似ています。

最初に用意されている系列に対する転倒数と出現回数を求めるパートがありますが、これは「既にできている系列に、長さ \(1\) の系列をくっつける」という操作を考えれば、上の計算方法を流用できることがわかります。

こういうやり方は meldable heap の push を meld でやるやつに似ている気がします。

L

偶奇で分けます。

配列 \(a\) を用意し、クエリ 3(一点加算)によって変化した値を持ちます。 クエリ 1・2 における変化はひとつの変数 \(x\) で持ち、クエリ 1 がきたら足し、クエリ 2 がきたら引きます。

\(a[2i]-a[2i+1]\) の形式で書ける値の個数カウンタと、\(a[2i-1]-a[2i]\) におけるそれを用意します。 出力クエリでは、それらのカウンタで \(x\) や \(-x\) の個数を調べればよく、終わりです。

添字関連でこわれるかなと思ったのですが、一発完動だったのでうれしかったです。

M

なんかこう、賢いをすればいいんだろうなとか、逆順に処理するのが定石だろうなと思いつつ、HL 分解で済ませました。

N

盤面サイズが定数でめずらしい。 サンプルを見ると最大ケースが載っており、いろいろ察します。

値の種類が二つのときの中央値は多数決と同じですね。そういえば最近 MAO の話を聞かない気がします。

包除をするのかな、いや無理かとか思いました。 あと、なぜか縦横がそれぞれ \(60\) くらいだと思っており、「bit DP は無理か〜」と思って棄却しました。

一旦 O を見てしばらく考えて無理になったあと bit DP でいいじゃんとなり、しばらくバグらせるも、なんとか AC できました。 一番上と一番下に 000000 を追加するといい感じになりました。

O

最小費用流?と初手で思って棄却したんですが、冷静に考えるとこれを詰めるべきだったと思います。 区間なのでそれを利用して(スキップするような感じで)辺を張るんでしょうとか思ったのに、なんで辿りつかなかったんでしょうね。

結局、in-place DP っぽく考えたいもののよくわからないとなり、実家になじめていませんねとなりました。

おわり

\(10^9+7\) にしなければ数え上げを出しても ok みたいなのを感じました。

動揺していた序盤と N 以外ではバグらせなかったのはよかったかなと思います。

満点を取るつもりだった人が 94 点で「エキスパートでよかった〜」と言っているの、ダサいですよね。つらい。

*1:厳密には無ではなくて、前のエキスパートが切れてから少しの期間は有効になるものは取得できることが期待されます。

ICPC 2020 国内予選参加記

これは夢日記ではありません*1夢日記これこれ

チームメイトは夢日記と同様、monkukui と TAB とえびちゃんです。 チーム名は tsutaj で、これは去年までチームメイトだった先輩に由来します。

tsutaj 抜きのチームに tsutaj と名付けるの、余因子展開とかで出てくる、行列 \(A\) の \(i\) 行 \(j\) 列抜きを \(A_{ij}\) と書くやつに似ている気がします。

See also: monkukui の参加記TAB の参加記

前日

バチャ をしていました。やや冷え気味ですね。

それからは、有名なライブラリをローカルに落とす遊びをしました(結局使いませんでした)。

去年のことを思い出したりしていました。去年の参加記は これ

当日・本番前

リハーサル

模擬地区のときはリハーサルに出そびれたので、今回はちゃんと出ました。 ちゃんとと言いつつも、AC はしていません。

f:id:rsk0315:20201106204750p:plain
うっかりね

せっかく「まれに見られる悲しい状態」*2になったので、おふざけの画像を生成したりしました。

矢印で表現するやつかわいくてすき。この生物には名前がついているようですね(ふににちゃん?)。

ジャッジ結果の URL を少し改変するといろいろなジャッジ結果を見ることができ、(少なくとも?)20 個あるようです。 プログラムと解答ファイルの両方にプログラムを指定すると以下のような結果になります(ペナなし)。

Program as answer.

You submitted a program source file as an answer file. Enter a file name of an answer file in the box.

両方に解答ファイルを指定すると Correct answer. と言われ、二つ目のケースで困ってしまいます。 たぶん、こういうお助け処理は correct 以外のときだけ行われるのでしょうね。

Binary program. というジャッジ結果もあるようなのですが、C++コンパイルした実行ファイルを提出したり、Java のクラスファイルを提出したりしても Correct answer. になってしまい、条件がよくわかりませんでした。

他にもいろいろ試そうと思っていたんですが、試し終わらないうちにケースを使い切ってしまいました。

そういえば、国内予選のジャッジ結果では、紫と緑が AC 時に出てくる背景色なのですよね。 そのことはわかっているのですが、文字色が赤なので、AC のときも一瞬焦ってしまいます(警告色を感じるので)。

20 種のジャッジ結果たち。 興味ある人がいるかわからないですが、せっかく調べたので載せてみます(載せない方がよかったら消えます)。

Congratulations!

You have successfully finished this problem.

Correct answer.

Proceed to the next data.

Correct answer.

Proceed to the next data.

Recall that to finish a problem, you need to correctly answer two data in a row, with the same program.

Correct answer, with a program different from the last one.

BAD NEWS: You can no longer finish this problem, because you would need to answer the next data correctly with the same program, yet there are no data left for this problem. Recall that to finish a problem, you need to correctly answer two data in a row, with the same program.

Correct answer, with a program different from the last one.

Proceed to the next data.

Recall that to finish a problem, you need to correctly answer two data in a row, with the same program.

Empty answer.

You either specified a wrong file name which does not exist, or an empty file. Enter a correct file name in the box.

Empty program.

You either specified a wrong file name which does not exist, or an empty file. Enter a correct file name in the box.

No answer file.

Enter a file name in the box.

Missing problem.

Select a problem in the list.

No program file.

Enter a file name in the box.

Wrong answer.

Try again.

Wrong answer.

Try again.

You need to correctly answer two more data. Recall that to finish a problem, you need to correctly answer two data in a row, with the same program.

Wrong answer.

BAD NEWS: You can no longer finish this problem, because you would need to answer two more data correctly, yet there is only one data left for this problem. Recall that to finish a problem, you need to correctly answer two data in a row, with the same program.

The contest is over.

You can no longer send your answer.

Binary program.

You specified a binary (executable) program or compiled Java class file. Enter a file name of a source program in the box.

Program as answer.

You submitted a program source file as an answer file. Enter a file name of an answer file in the box.

Input as answer.

You submitted an input file as an answer file. Enter a file name of an answer file in the box.

Input as program.

You submitted an input file as a program file. Enter a file name of a program file in the box.

Previous answer.

You submitted an answer to the previous data. You should run your program with a new input file.

Submits in too short time.

The server stopped processing your submit since you made submits in less than 0.5 seconds.

以上 20 種です。

間違ったファイルを指定してしまったとき用のジャッジ結果が用意されていて優しいと思います。 Wrong Answer. 以外はペナがつかないようになっていると思います。

リハーサルおわり

暇なので、バチャをしていました。 肩慣らしに簡単な問題を解こうということで、チームで これ をしました。

えびちゃんが H を AC した後に monkukui が H に RE を投げていたりして面白かったです。 J は苦手そうだったのですが、ひらめいて、書いてすぐ投げました。数秒後に monkukui が WA を投げていて危なかったです。

当日・コンテスト

ざっくりとした戦略は次のような感じです。

  • monkukui が A、えびちゃんが B、TAB が C を見て、各々できたら書く。
  • 構文解析はえびちゃん、幾何は TAB、その他つらそうなのは monkukui に変わる。
  • できるなら一人でやるが適宜話し合う。
  • デバッグが必要になったらえびちゃんに聞く。

A や B でペナを出すとつらいので、油断した実装をしないようにしましょうという話をしたり、「まぁ WA を出しても後半でもう 1 問解いてくれたらいいからね」とか言ったりしました。

問題内容はあまり説明しないので、必要に応じて 問題文 を読んでください。

開始

あの、はじまりません。

no status information for team ###### in contest are found (database broken or not initialized?)

と書かれていたので、たぶん database broken or not initialized だったんだと思います。

「れれれ冷静になっていいい一旦コーチに連絡しましょう」とか言っていました。えびちゃんが一番焦っていたかもしれません。

今度こそ開始

B を読みます。union-find を貼りそうになりますが、必要なくて、std::set で処理して、AC です。 数秒差で A も AC して、5 分くらいで 2 AC です。この時点で 6 位です。

C、え、これ、何するんでしょうね。

D は構文解析らしいので、うれしくなって、読みます。 え、これ、何するんでしょうね。

E は monkukui が解けそうと言ったり、解けてないことがわかったりしました。

正直なところ、2 完でお通夜まであるかなと思いました。 最初は「全チーム高々 2 完で終わるからペナ差で通過でしょ」と思っていたのですが、めちゃくちゃ C が通されていて焦ります。

D は、愚直には階乗通り試せばいいんですが、指数時間に落としたい気持ちです。 階乗モノは bit DP でできがちというのがあったので、変にハマってしまいました。

C が謎なんですが、TAB が愚直を書いて回してくれて、時間は掛かったものの AC してよかったです。 この時点で 68 位とかになっていて、ボーダーギリギリで通過かなと思っていたんですが、どうやら学内 3 位らしく、あーとなりました。

D は TAB と話しながら決め打ちというワードまでは出てたんですが、なかなか時間がかかりました。 解法は以下のような感じです。

最終的な答えとなる変数を決め打ちする。 残った変数について、上記の変数より前に来るか後に来るかを決め打ちする(指数通りある)。 答えの変数には 0、前のものには -1、後のものには +1 を割り振る。 (x<y)min(x, y)(x>y)max(x, y) として計算する。 どちらかの式の値が 0 以外なら寄与分は \(0\)。 両方の式の値が 0 なら valid な順序であり、前のもの・後のものの個数をそれぞれ \(f\), \(b\) とすると、寄与分は \(f!\cdot b!\)。 これを、各決め打ちに対して足し合わせたものが答え。

各順序に対して、それが答えに寄与するのは(ある変数が全体の値となる)1 回だけで、両方の式の値が一致するときのみ加算されるので、うまくいきます。

コンパイルが一発で通り、サンプルも一発で合い、テストケースも二発(国内予選なので)で通り、気持ちよかったです。 頭と手がふわふわしました。 「D、サンプル、通った」「あ、correct って出てる」とか、かなり口が不自由になっていたと思います。

31 位くらいになっていて、さすがに通過したかなと思いました。

TAB が F の概要を説明してくれましたが、頭がふわふわしていたので半分くらいしかわかりませんでした。非常にごめん。

前後関係をあまり覚えていませんが、monkukui が E をがんばっていて、TAB がサポートしたりしていたと思います。 桂馬なので二部グラフになるとか、一つ飛ばしで動くので偶奇で分けていいとか、左右に動く(まっすぐは動かない)のでそこでも半分にできるとか言っていて、賢いなぁとなりました。

monkukui ががんばってデバッグして、えびちゃんが手伝って、結局えびちゃんの PC で計算して、E を AC しました。 この時点で 12 位とかで、夢かと思いました(夢地区ではないです)。

頭がふわふわしていて、Congratulations! を見ながら「あ、E 通ってますね(他人事)」と発言してしまいました。 自分でも意味不明すぎて二の句が継げなかったのですが、想像以上にチームメイトを混乱させてしまったようです(それはそうか)。ごめんなさい。

うっかり「通過したでしょ」と口走ってしまったのですが、ここから弊学の他チーム全てに抜かされると不通過になるはずなので、それらのチームに対して失礼な発言だった気がします*3。ごめんなさい。

F を TAB ががんばっていたもののバグっていそうで、デバッグを手伝うも、うまくいかず、そこで終了しました。

提出した問題については一つも WA を出さなかったのもよかったと思います。

最終的な順位は 18 位でした。

icpcsec.firebaseapp.com

これ、毎年同じ URL なので、来年には見れないんですね。

コンテストおわり

「もう TweetDeck 開いていい?」とか言っていました。やばい人じゃん。

2 完を覚悟したタイミングから各々 1 問ずつ AC していて、喜ばしい気持ちになりました。 あれこれ話し合ったりしてチーム戦っぽさもあったと思います。 特に E は TAB が考察・monkukui が実装・えびちゃんがデバッグ(と提出)をしていて、面白かったです。

今回の国内予選は今までの国内予選で一番気持ちよかったと思います。 理由は以下のものが主です。

  • 4 年連続で通過できたこと
  • 本番中に構文解析を通せたこと
  • 構文解析をバグらせなかったこと
  • 20 位を上回ったこと
  • 5 完できたこと

去年までできなかったことがいろいろできたので、うれしかったです(通過自体はできていたけど 4 年連続は初めてなので)。 三人コーダーのチームなので並列 OK のルールだと有利なのかなとも思いますが、最近の人たちはほとんど三人コーダーだったりするかもしれません。 何にせよ、うちのチームにとってはやりやすかったと思います。

TAB が「あまり仕事できなかった、愚直しか書いてない」と嘆いていましたが、後半 3 問の AC は TAB のおかげだと思います(愚直のやつ含む)。

毎年、終わってしばらくぼんやりしてから「どうやら例年通り通過したらしい」という実感が湧いている気がしますが、今年もそうなりました。

f:id:rsk0315:20201106222615p:plain
自画自讃ではない

tsutaj ありがとう。

おわり

おわりです。やっぱりうれしい。

今年は弊学からは 2 チーム通過はならなかったように見えますが、来年も出場資格がある人はぜひがんばってほしいと思います。

*1:念のためほっぺたをつねっておきました。

*2:ICPC 予選システム ユーザマニュアル にある表現。

*3:それらのチームがここから十分な個数の AC をすることはないと発言しているように聞こえるので。

ICPC 2020 夢地区参加記 2

参加記 1 は これ。 意外と好評でした。

シリーズ化するとは思っていなかったんですが、また参加してしまったので書きます。

ここから夢です。

今回は、非オンサイト・問題文が日本語だったことから、国内予選相当だったと思うのですが、前回との時系列はよくわかりません。 夢地区では一年に複数回大会があるのかもしれません。

「Dream Regional 国内予選」のような感じだと思うので、「夢国内」ではなく「夢地区」のような呼称でいきます。

前日

夕方頃に友人にゲーセンに誘われましたが、「明日予選だから早く寝る」と言って断りました(謎ムーブ)。 なかなか寝つけなかったので、その後一人でゲーセンに行きました(最悪)。

あまり覚えていませんが、24 時くらいに寝たと思います。

Contest

どうやら、考察用紙も採点の対象らしく、特定の内容(コンテスト中にアナウンス)が書かれていると AC 数が 1 増えるらしいです。 どうやって提出するのかはあまり覚えていません。

えびちゃんは A と E を解き、TAB/monkukui が B と C を解きました(!)。

問題は、虚無なんですが、一つだけ覚えているので一応書きます。

\(10^{100}\) 個のマスが一列に並んでいます。 左から \(i\) 番目 (\(0\le i\lt 10^{100}\))のマスは、\(i\) が \(15\) の倍数のとき白、そうでないとき黒で塗られています。 あなたは左から \(x\) 番目のマスにいて、今いるマスが黒い限り右に動き続けます。 あなたが止まるマスの番号を求めてください。

\(0\le x\le 60\)。

ええと、切り上げ除算をして解くのが楽だと思います。 夢の中のえびちゃんは、なぜか std::bitset_Find_next() などを使っていました。

順位表を見ると、ボーダーギリギリくらいだったので、できれば AC 数を増やしたいです。

さて、考察用紙キャンペーンのアナウンスがあって、考察用紙にねずみがたくさん書かれていることが条件らしいです(意味不明)。 何が要求されているかよくわからないので、「ねずみ鼠ネズミ🐭ねずみ」などとたくさん書きました。

ねずみを書きながら monkukui と話していると、どうやら誰も D を読んでいないらしいことに気づきました。 ねずみを書きながら D を考えていると、DP で解けそうに見えました。

実装しようとしたところ、PC がロックされており、ログインパスワードを要求されていました。 えびちゃんはパスワードの存在自体知らず、(すでに数問解いたはずなのに)最初どうログインしたのか謎です。 monkukui に尋ねようとするも、失踪しており、探しているうちに起きちゃいました。

振り返り

途中で起きちゃったので、謎キャンペーンの結末は謎のままです。

前回の反省は活かされていて、会話をする・順位表を見る・コーディングをする・問題文を読むを全て行えています。すごいね。 失踪はしない方がいいと思います。

読者の声

WF は出場したことがないので、夢に出てくるかわかりません。 と思ったのですが、出場したことがある国内・地区予選の再現度がご覧の有り様なので、夢 WF も参加できるかもしれません。

なんと今回問題が解けました。 あと、問題を解かなくても AC 数が増えるルールの存在も明かされました。

AC 数が 0 でも予選突破できるルールもあるかもしれません。

夢の内容自体より、夢の内容をこんな形で公開していることに対して、心配になってきますね。

おわり

お粗末さまでした。

ICPC 2020 夢地区参加記

ICPC に夢の中で参加してきました。せっかくなので参加記を書きます。

期待しないでね。

チームメンバーは monkukui と TAB とえびちゃんです。 問題文が英語だったので(国内予選ではなく)アジア地区に相当する大会だったと思います。 オンサイトで、多くのチームが集まっていました。

これは夢の中でのできごとなので、設定などが現実に即するとは限りません。 実際の ICPC では避けるべき行為が横行しています。

Practice session

特にありませんでした(夢だからね)。 前夜祭とか諸々の描写もありませんでした。

Contest

えびちゃんが A、TAB が B、monkukui が C を読みます(開始直前の話し合いで元々の予定と担当が入れ替わりました。この時点で不穏)。

A の問題文が長くて、なかなか把握できません。 そんなのに数十分も掛けていると手汗や動悸が大変なことになります。

落ちつこうと B 問題に目をやる(ここ謎ムーブ)と、IP アドレス関連の問題らしいです。書いてある通りに実装がんばる系?(イメージ

もう少し B を読むと、「IP アドレスの説明だけは英語で書くけど、問題文自体は IP アドレスを使って書くよ」とか書かれていて謎です。 IP アドレスってそんな表現能力なくない? 図とかが描かれていたので、エスパーが要求されていたのかもしれません。

この辺でようやく気づいたんですが、どうやら PC は用意されたものではなく各自のものを使うルールらしいです。 コンテスト開始時に電源をつけていてよかったらしいのですが、ルールを把握していませんでした。 慌ててバッグから Mac を取り出そうとしたのですが、混乱しており、ゲームボーイを取り出してしまいました(ドラえもん状態)*1

その後、大きめの模型か何かが各自の机に配られました。 よくわからなかったのですが、問題で必要になるのかもしれません?(三次元幾何かなにかのサンプル? やさしいね。) これが非常に邪魔で、机の上のフリースペースが問題文数行ぶんしか無くなってしまいました。 上記の各種トラブルも相まってすでに半泣きです。

(追記 2021/03/18:例年の ICPC アジア地区もそうだと思うのですが、問題文は紙で配られました。)

スペースが狭いのもあって、ペンを落としてしまいました。 他チームの近くまで転がっていってしまったので、スタッフの人に拾ってもらおうとしていたのですが、気づいたら他チームの人に取られていました。 代わりに、インクが出なくなったペンが数本置かれました(いじめ?)。

つらくなって Slack を見てみる(不正でしょ)と、tsutaj から「1h18m 経ったけど AC まだ? 問題文の翻訳できた?」とメッセージが来ていました。 「競技中だから問題文の内容は教えられないよ」と返答したのですが、これはいろいろとおかしいです(別に問題文の内容は聞かれていない・不正をしつつもルールを気にしている)。

ところで、コンテスト開始以降 monkukui/TAB は無言だし、えびちゃん以外も進捗は芳しくないようです(会話をしようね)。 自チーム・他チームの AC 状況も知りません(順位表を見ようね)。 Slack 以外で PC を触った記憶もありません(コーディングをしようね)。 あと最後まで A 問題を把握していません(問題文を読もうね)。

そんなこんなで、結果も心もめちゃくちゃになってコンテスト終了です(??)。 本番はこんなことにならないように気をつけましょう(???)。

Closing ceremony

やる前に目が覚めました(夢だからね)。

おわり

お粗末さまでした。 夢の内容としてはトラブルや不正が主で、問題文をほとんど読めなかったのが悲しいです。

なんでこんなのを記事にしているんだろうと思いつつ、がんばって書き切りました。

*1:Macゲームボーイは似た形状をしている世界線らしかったです。現実のそれらとは似ていませんでした。なんかこう、太いパイプ状のものがついていました(?)。

あとでなおす

競プロをしていて、一時的に値を決めておくけれども、提出時には直したいということがあるかもしれません。何らかの理由で。

int m = 10;  // to be edited

実際には m は入力によって変えるべきだけれども、手元のデバッグなどの都合では定数の方が楽、とかがあるかもしれません。 で、直さずに提出してしまうと WA なり RE なり TLE なりになって、ペナが生えてしまいます。

CE ではペナが生えないので、以下を満たす書き方を目標とします。

  • 手元では動く
  • 提出すると CE になる

あと、C++ を考えます。Rust でも頭をひねればできるんじゃないかな。

前提知識

AtCoderCodeforces では、ジャッジ環境において ONLINE_JUDGE#define されます。 AOJ ではこれがされないはずなので、別途考えます。

で、#ifdef neko というのによって、neko#define されているかどうかが分岐できます。 あるいは #if defined(neko) でもよいです。

#ifdef ONLINE_JUDGE
// ジャッジ側ではコンパイルされる
#else
// 手元ではコンパイルされる
#endif

AOJ ではこれがされないので、手元で適当なマクロを #define するといいです。 #define をコード中で行うと提出時に書き換える必要があってペナの原因になるので、コンパイルオプションで変えるとよいです。

% g++ neko.cpp -DLOCAL

オプション -Dneko をつけることで、neko#define された状態にできます。 手元ではそうやってコンパイルすることにして、それを使って #ifdef で分岐できるようにしておけばよいです。

AOJ にも対応させられるようにするなら、AtCoder たちについてもこれを使う方が楽だと思います(分けなくて済むので)。

実装

以下のように書けることを目標とします。

int m = tbe(10);

こう書くと、手元では m = 10 として動いて、ジャッジでは CE になってほしいです。

以下のような関数を書いてみましょう。

#if !defined(ONLINE_JUDGE)
template <typename Tp>
Tp tbe(Tp const& x) { return x; }
#endif

AOJ で使うなら、こうです。

#ifdef LOCAL
template <typename Tp>
Tp tbe(Tp const& x) { return x; }
#endif

これをすることで、手元でだけ tbe が定義されていることになります。

ねこ

こんな感じでいいですか?

追記