AHC 034 に参加しました。グリッドに +-100 の値が設定されており、効率良く 0 に均す問題でした。いつも通り適当にパスを作ってうろちょろすることしかできず、成長の仕方が分かりません。
yosupo さんが 1 位でした。かっこいい。 トヨタ自動車プログラミングコンテスト2024#6(AtCoder Heuristic Contest 034) 解法 - よすぽの日記
ABC 359 に参加しました。
| 問題 | A 問題 | B 問題 | C 問題 | D 問題 | E 問題 |
|---|
| 予想 | 10 | 300 | 1,300 | 1,100 | 1,500 |
| 実際 | 10 | 27 | 828 | 1381 | 1275 |
Table 1: Diff 予想なんと文字検索の問題でした:
main=interact$show.length.filter(=='T')
i∈[0,2n−3] の範囲で見ます:
main=interact$show.sum.f.map read.words;f(n:x)=[1|i<-[0..2*n-3],x!!i==x!!(i+2)]]
zipWith を使うが上手い方法ですね:
無理過ぎる……
直前 K 文字の状態を持って DP します。 K 文字目までと K 文字目以降で処理が変わるのが厄介なところです。
i 番目の壁を乗り越えるためには、 j≤i(∀k∈[j,i],A[k]≤A[i]) を高さ A[i] まで埋める必要があります。また j−1 番目の壁を乗り越えるためには、 dp[j−1] の水が必要です。
Max モノイドのセグメント木を持ちます。上記の j を 2 分探索によって発見します。計算量は O(Nlog2N) ですが、簡単で良い方法だと思います。
区間 Max をどこまで伸ばしていけるか判定するためのモノイドを作成します。計算量は O(NlogN) ですが、真横のモノイド同士しか結合できないなど、細かい判定がかなり大変です。
同様に、重い方針だと思います。
なるほど。ここでスタックは償却計算量 O(1) (1 回のクエリで最大 1 つのみ要素が追加され、それぞれの要素は最大 1 度のみ使用される) であり、 O(N) で解けるみたいです。
難しそうですが……。
AtCoder Library Practice Contest (ALPC) を埋めました。最後に残った問題が floor_sum でした。 floor_sum の解説は、 【math編】AtCoder Library 解読 〜Pythonでの実装まで〜 が良かったです。この記事の図を写経して理解しました。
以下は 100% 自分用のメモです。
TODO: 誤記修正 (多数)
Floor sum は 1 次関数 y(x):=max+b(a,b∈Z≥0,m∈N) の下側にある格子点の数を高速で数えるアルゴリズムです。具体的には、 S:=[(y,x)∣x∈[0,n),y∈(0,y(x)]] 中の格子点の数を O(logaN) で求めるアルゴリズムです。
S を 3 つに分けて計算します。

- x, y の変域は半開区間であるため、 S の下端と右端には格子点がありません。
- 緑箇所 S1:=S∩y≤mb 中の格子点の数は n⌊mb⌋ です。
- ⌊mb⌋ は y(x) の y 切片の整数部分です。
- y>0 に注意します。
- 赤色箇所 S2:=S∩y>mb 中の格子点の数は i=1∑n−1⌊ma⌋ i です。
- y>mb より S1 と S2 は共通部分を持たず、格子点のダブルカウントを避けられます。
- y=⌊ma⌋ は y(x) と共通の y 切片を持つ傾きが整数の直線であって傾きが最大のものです。 y≥0 を保つ限り y=ma から y=x を引ける回数が ⌊ma⌋ であるとも言えます。
- y 方向の変域 (mb,mb+⌊ma⌋x](x∈N) の大きさは、たとえるならば 整数値−eps です。よって S2 中の格子点の数は y 切片の値によらず常に i=1∑n−1⌊ma⌋ です。
残る S3:=[(y,x)∣x∈[0,n−1],⌊ma⌋mb<y≤y(x) を考えます。 y′:=y−⌊mb⌋ にフォーカスします。このとき元の直線は y′=ma+mb%m となります。

実はダルマ落としのように赤い三角形部分を切り落としてしまっても、 S3 からの格子点の数は変わりません。元の直線の傾きから赤い直線の傾きを引き、青い直線 y=ma%m+mb%m:=y′(n) が現れます::

- 紫箇所は格子点の数に寄与しません。そもそも y′′ 軸が y′=y′(x) 中の最大の整数部分で切る線だからです。
- 黄色箇所は格子点の数に寄与しません。なぜなら青い直線の傾き ma%m<1 かつ y 切片 mb%m<1 のため、点線を伸ばしていくと x=−1 に到達する前に y=0 になります。
ここで x,y′ と x′′,y′′ の関係を整理します。
xy′=−y′′+n=−x′′+⌊y′(n)⌋これを肝心の直線 y′=ma%mx+mb%m に代入して整理します:
−x′′+⌊y′(n)⌋ma%my′′y′′=ma%m(−y′′+n)+mb%m=−ma%my′′+ma%mn+mb%m=−ma%my′′+y′(n)=x′′−m⌊y′(n)⌋+my′(n)=x′′+m(y′(n)−⌊y′(n)⌋)=x′′+((a%m)n+b%m)%m=a%mmx′′+((a%m)n+b%m)%m- y′(x)=m(a%m)x+b%m:=mf(x) に対して m(y′(x)−⌊y′(x)⌋)=f(x)%m
上図の x′′,y′′ 座標系に注目します。紫部分を除去し黄色部分をくっつけることで、元の問題と同じ形に帰着できました。よって floor_sum は再帰的に計算できます:

floorSum(n,m,a,b)=⎩⎨⎧0 if m=0,0 if a=0,fromS1+fromS2+floorSum(n,m,a%m,b%m) otherwise.再帰関数を書きました: Math/FloorSum.hs.
末尾再帰 & 正格評価の形 (返値を !acc 引数として引き回す形) に書き直しても、ジャッジ環境においては高速化されませんでした。 Haskellで再帰を心置きなく書いて良い理由 を見る限りでは !acc を書いて初めてサンクを潰せるように見えますが、実はコンパイラがよしなに正格評価してくれる気がします。関連: foldl vs. foldl'に終止符を打つ 。
傾きが大きいほど計算量が増え、 O(logma)O(\log ma)O(logma) らしいです。たぶん互助法の計算量と同じ? 今は理解する体力が残っていません……。
消耗しました。この程度の数学に 1 週間を丸ごと費やすとは……絶望ですw
上記の floorSum において a≥0,b≥0a \ge 0, b \ge 0a≥0,b≥0 の成約を取り外すことができます。応用的な問題で役立つらしく、これも読み込みたいです。
4.9. a, bが負の場合