Closed117

典型盆栽 (Haskell 盆栽 3) → 入水ならず

toyboot4etoyboot4e

このスクラップは

本やガジェットをひけらかしつつ、水色コーダーを目指す投稿です。

方針

水 diff を 100 問解けば水色コーダーになれる説 (願望) を検証します。スクラップ作成時の進捗は 32/100 です。

注意

Haskell 目当てなら、もう見所が無いと思います……!

toyboot4etoyboot4e

良かったもの

ガジェット
  • Kindle Scribe
    オライリー本を超えた大画面で技術書を読みます。
  • AFB7218
    適度な負荷で読書週間を作ります。
本 (Haskell)
本 (競プロ)
Podcast
Youtube
toyboot4etoyboot4e

消化用ノート

PAST まで

  • 水 diff streak (ABC 255 まで)
  • 残件処理
    • 鉄則本 ゲーム
    • Chokudai Speedrun 02 残り
    • PAST 第 8 回 残り
  • PAST (6 月) 対策
  • PAST 上級本
    • 遅延セグメント木
    • 最小費用流
    • 平面走査

PAST 後

  • PAST 上級本 2/2
    • 考察テクニック
    • DP
  • 水向け演習
    • TDPC (既知の範囲)
    • 典型 90 問 ☆ 4, ☆ 5
  • もっと DP
    • EDPC (未知の範囲)
    • TDPC (未知の範囲)
  • 典型収集漏れ
  • 過去問がすべて解ける考察フローチャート作成
toyboot4etoyboot4e

典型ノート

水 diff 演習が落ち着いてから、取りこぼした典型問題を拾って行きたいと思います。

少なくともこれだけ零しているのか……怖すぎる!

回収済みの解法
  • Brute-force 全探索
  • 2 分探索の判定問題で最大化・最小化
前提条件となるテンプレート
  • 素数列挙、素因数分解、 ModInt
  • セグメント木、 Union-Find
  • 2 分探索
お手軽テンプレート入り
  • 座標圧縮 (1 次元)
  • 尺取り法
  • 転倒数 、辞書順
  • 作用素付きモノイド
    • ダブリング
    • 全方位木 DP
  • Rolling Hash
  • topSort, SCC
  • dfs, bfs
  • dijkstra, 逆グラフ上の dijkstra
得るべきテンプレート
  • 2 つの配列をまたぐ尺取り法のテンプレート?
  • Rollback 付き Union-Find
  • 遅延セグ木
解けるべき典型
避けていた問題
  • ゲーム
    • Nim ゲーム
    • Grundy 数
DP をもう一度
  • 集合 DP (Bit DP)
  • 桁 DP
  • 区間 DP
  • 期待値 DP
高度典型
  • 2 次元の Union-Find
  • Mo
  • FFT
  • 重心分解
  • HL 分解

https://ei1333.github.io/luzhiled/

toyboot4etoyboot4e

日報 2023-05

第一週 (連休): 水 diff streak 敢行中
  • 05/01: エアロバイクを注文した。

    • ABC 300 D: 基本全探索だが、オーバーフロー対策が必要だったのが難しかった。
  • 05/02: エアロバイクを漕いだ。

    • ABC 276 F: オーバーフローが原因で WA になった。解法は DP + 区間木で PAST にも出そうな典型だったが、 PAST と違って時間制限が非常に厳しい。時間制限を意識するべきか。
  • 05/03: バイクを漕いだ。 2 年で壊れるなら月あたり 3,000 円なので割に合わない気もする。

    • ABC 266 F: 数時間かけて力尽くで解くも DDoS 攻撃でサイトが落ちている。ローカルにコピーを取る意味でも AtCoder の TUI が欲しいかも 。 → AC. サイクルを見つける、サイクル上の頂点を集める、なとグラフ上の手続きが非常に大変だった。標準でスタック・キューが無いことも辛さに拍車をかけている。やはり時間内の AC は到底不可能で、水色 100 問でも水色コーダーは厳しいかもしれない。稀に青パフォが出せるくらいじゃないとな……
    • ABC 259 E: LCM はマージして求められることから、区間木を使用する。区間木に Unbox ではない値も載せられるように改造できたのが盆栽的に非常に良かった。しかし Set でユニークな要素を判別しようとして TLE (AC: 33/43) 。 Set を使っているから、というよりも、リストのマージ処理で LCM を求めるのが遅すぎるのかもしれない。リストのマージ処理を IntMapunionWith に置き換えたところ、全く同様にTLE (AC: 33 /43) . 解き方が根本に悪いのか……? 区間木を左右からの累積和に変えても同様の TLE (AC: 33 / 43) 。
      Qiita の解説 通り、特徴量のみを捉えれば良い。解説 AC はできたが完全敗北。 TLE の原因を考察したい。素数の種類は最大 N のため、マージ処理はならし O(1) に見えるが……
      まあいいか!
  • 05/04: バイク。 ABC 300 (A ~ E) の解説 を書いた。

    • ABC 261 F: なんだかんだ近いコンテストでは類題というか似た問題が出る模様。一見区間木で解けそうだが、 ABC 259 E と同様に TLE してしまう。 WA も出て心が折れそうだ。これも解説を見てしまおう。
      公式解説が難しかったので zenn の解説 をあたる。まず転倒数の和が色を無視した入れ替え操作回数になるみたい。後は色ごとの転倒数を引けば答えになると。なるほど……やはり考察失敗だった。
      それはそれとして TLE が取れない。 → 転倒数の計算前に座標圧縮したら間に合った。アロケーションを N 回繰り返すため、 O(N^2 log N) の処理になって遅かったみたい。座標圧縮で N log N 程度に納められたはず。
      zenn の解説では 1 つの区間木を使い回していたが、それで間に合うのがかなり不思議。 resetSTree もアロケーションと同様に O(N) の処理で TLE となった。 あ使用した添字のみ 0 にリセットして通しているみたい。これなら O(N^2) (もしくは O(N^2 log N)) ではなく O(N log N) で済む。同様に効率的なリセットをすると通った。まあ座標圧縮版が楽で良いか。
  • 05/05: バ

    • ABC 262 D: 大体全探索だと思いますが、再帰的リスト生成が苦手。何とか提出するも TLE. 解説を見て考察失敗だなと……。これで水 diff 下位とはどういうことなんだ。。
      1 つずつ処理するデータ範囲を広げていく普通の DP を n 回実行! 時間制限ギリギリで AC できた。これが brute-force DP か。全探索を信じよ!
    • ABC 250 F: IntMapscanl' したり区間木に入れて失敗するシリーズ。このアンチパタンめちゃめちゃ多いな。そこからどう高速化するかは多種多様だけれども。。
      今回重そうなのは比較だが、普通に比較すると O(N^2) なんだよな。。 → 比較結果を配列に入れておくだけで通ってしまった 。本当にそれでいいのか……?! 最低限解説は読んでおこう。
  • 05/06: バ

    • ABC 255 E: 最初の要素を決めると以降の要素が一意に定まる。最初の要素を決めた後のスコアは N logM 程度で計算できるため、最初の要素の候補を絞れば良さそう。カウントが最大の値を『ラッキナンバー』のいずれかに合わせるような x をすべて調べれば良いか。 → TLE. ハッハッハッ。確かに、たとえば重複 0 だったら N^2 log M だ。。
      全然解けないな。高速化したら WA まで出ちゃったよ。候補を絞り込んだのがダメだったみたいで完全に考察失敗だ。今週ずっとこんな感じだわ!
      いやカウントはループを逆にして M log N 程度で計算できるな。 NM log N くらいで解けるのでは。 → 解けた。やはり天才だったかw
      終わってみれば brute-force 全探索。この程度の問題を反射的に解けないと水パフォは厳しいか……。
    • ABC 258 E: 箱の数が異様に多いため、規則性を見つける必要がありそうです。ループを見つけるための試行回数は最大 N 回。あるいはダブリングした方が簡単そうですね。ダブリング用の配列は尺取り法のテンプレートであっさり求められそうです。
      → WA. くっ……! → AtCoder Companions を見ると 確かにループの計上を忘れていた……。再度提出するも WA2. 再び AtCoder Companions を見ると 確かに尺取り法が境界の場合を見逃していた……。実践基準で考えると全然惜しくないよ。。
      ど典型だと思ったけれど解説はダブリングじゃない……? ああループ。テンプレートがあるなら、ダブリングした方が簡単かなとは思う。必ずループする (行き先がダブる) というのは鳩の巣原理と呼ばれるそうで、最近の黄 diff でもあったみたい。
  • 05/07:

    • ABC 257 E: まず桁数を最大にして、次に大きい桁から最大化していくという貪欲解で十分そう。 → WA (AC 21/30). えぇ WA か……。考察ミスで WA が多くて痛いな。
      → テストケースを作ってみたら反例が見つかって AC. 最小コストで埋めた桁より小さい値に書き換えてはいけない。
      制限時間が怪しいものの完全勝利! まあ D で出たら緑 diff になった気がする。
    • ABC 298 F: 以前解説を見ていたおかげで余裕で解けてしまった。枝刈り万歳!
    • ABC 292 F: 解説 AC じゃないと無理だった。
      四辺形の短辺に正三角形の底辺を合わせる。角 \theta だけ三角形を回転させる。三角形の 1 つの角を四辺形の辺に合わせる。もう片方の角が四辺形からはみ出ないように可能な限り回転させる。
      なぜか acos1 / \cos だと思っていて苦戦しました。
    • ABC 293 E: 線形な漸化式の一般解を行列累乗で求める。行列累乗は行列の積のダブリング (繰り返し二乗法) によって求まる。この公式解説を読むと、 フレンズさんの解説 (Twitter) も同じものだったことが分かった (演算子のダブリングだった) 。
    • ABC 278 F: 木探索として考察して解いた。辺を区別しない方が良い。辺を区別すると、計算量が莫大に増える場合があって良くなかった。アルファベットを頂点とし、辺を消費しながら探索していくと爆速。 DP で解くのは大変そうなので止めた……。
第 2 週: 水 diff streak 完了、典型 90
  • 05/08: バ

    • ABC 268 D 水 diff 全探索。答えを見て寝かせていたのでスルっと AC. これが本番で解けないといけないんだよな……!
  • 05/09: バ

    • ABC 270 D: 水 diff 貪欲……では答えを最大化できない。ゲームは苦手、ゲーム x DP も苦手。鉄則本や EDPC でも飛ばしてしまった。 ABC は必ず弱点を突いてくるぞ……! これにて ABC 255 ~ ABC 300 の水 diff は全消化完了。
  • 05/10: バ

    • 典型 006: セグ木で楽をしました。 他の解法 も見ておくべきですが……!
    • 典型 013: 鉄則 C 問題でやりました。
    • 鉄則 021: どう見ても SCC. ここまで瞬殺だな……。
  • 05/11: バ

    • 鉄則 029: テトリス的な問題。 imos 法の累積和でもない。 O(N^2) の解答しか思いつかずサレンダー。
      セグメント木だと 1 点の値更新が O(log N), N 点の値更新は O(N log N) となる。しかし遅延セグメント木だと区間更新も O(log N) になるとか。 PAST 上級本を読もう。
  • 05/12: バ

  • 05/13: バ 30

    • [典型 051]: ☆ 鉄則本でやったはずだがノーマークだった。どう頑張っても通せず。
    • ABC 301: 4 完 (ABCD).
  • 05/14: バ 25

    • ABC 301 E: グリッド上の BFS + Bit DP. グリッド上の BFS はテンプレートに追加。
    • 典型 043: ABC 246 E の 01-BFS と同様。普通のグラフ探索で解こうとすると TLE しがちで難しい。 01-BFS を履修、枝刈り BFS を TODO 似追加
toyboot4etoyboot4e

400 AC

水 diff は 40 問解きましたが、 9 割方解説 AC でした。非常に厳しい……!


AC heat map (max difficulty)
水 diff は 1 日 1 問が限界のため演習期間がやたら長い

難度感

本番は時間制限が厳しいのが気にかかっています。水 diff までは trivial という感覚に到達しないといけません。今は茶〜緑も大分怪しいけれど……。

これまでの主な学び

全探索を信じよ!

今後の方針

もうすぐ演習量が こちらの色辺記事 に匹敵してしまいます。この先三ヶ月で現在のタスクをすべて消化し、それでも色変が厳しい場合は、 ARC 特攻するか高度典型を狙うしかないです。。

toyboot4etoyboot4e

水 diff streak 完了

ABC 255 ~ ABC 300 の水 diff の問題を全問解ました。

  • 手札があっても解けないことがしばしばありました。
  • 典型漏れは必ず突かれます。僕の場合はゲーム DP とか……。
  • 判定問題の 2 分探索が発想にありませんでした。考察フローチャートを作ってみたいです。
  • 転倒数もまず思いつきません。

次は典型 90 問を回って、 ★ 5 が半分以上自力で解けたら言うこと無しなんですが……!

toyboot4etoyboot4e

WIP: 木の実装を読んでみたい

エアロバイクと Kindle Scribe のコンボで情報収集します。

Fast mergable interger maps

IntMap の元ネタであるという Chris Okasaki and Andrew Gill. Fast mergable integer maps, 1998 を読みました。こういう実装紹介の論文 (論文?) はエアプに最適です。

  • 解説コードは Standard ML です。
  • 完全二分木から patricia tree (基数木?) へ。
    • 整数値キーの完全二分木は、各 branch が各 bit の 01 の分岐に相当します
    • 完全二分木をうまく圧縮すると patricia tree になります。へー
  • lookup, insert, union が再帰的になるのが分かりました。
  • 平衡処理が無くてあれっとなりました。
    • 確かに IntMaplookup とか insertO(\min(n ,w)) ですね。
    • ここで W とは木の最大の深さ (64) なので、操作コストは定数です。
    • まあ平均したら O(\log n) なんじゃないでしょうか。
  • 赤黒木は検索・挿入は速いがマージが遅い。 IntMap (big-endian patricia tree) はマージもそこそこ速い。

QuickChecking patricia trees

上の論文には バグがあるらしい ので、その修正を含むという Jan Midtgaard. QuickChecking patricia trees, 2018 を読んでいきます。

  • QuickCheck で patricia tree をテストして正しさを保証、しようとして上記論文のバグを発見した
  • テストにはプロパティベースのテストとモデルを使用したものがある?
  • WIP
toyboot4etoyboot4e

Haskell ネタ

思い切って入力処理を短い名前に

こんな感じにしてみます。

main :: IO ()
main = do
  -- list
  [n] <- ints
  -- tuple
  (!x1, !x2) <- ints2
  -- vector
  !xs <- intsVU

  putStrLn "TODO"

kill-emacs に学ぶ trivial 解への分岐法

競プロ限定なら exitSuccess すれば良し! やっぱり Emacs Lisp は勉強になるなぁ ()

import System.Exit (exitSuccess)

main :: IO ()
main = do
  let isTrivial = True

  when isTrivial $ do
    putStrLn "trivial!"
    exitSuccess

  putStrLn "run a non-trivial solution"
toyboot4etoyboot4e

Haskell ネタ 2

高速化 3 本立てです。

1. Vector の unsafeRead, unsafeWrite が効いた (~200 ms)

典型 90 問: 031 の TLE を取るために、セグメント木内の VectorunsafeRead, unsafeWrite で読み書きするよう変更しました。そんなものがボトルネックに……?!

デバッグ済みのライブラリでは unsafeRead, unsafeWrite を使うようにします。デバッグ専用のアサート文は残しておきます。

dbgAssert
#ifdef DEBUG
dbg :: Show a => a -> ()
dbg !x = let !_ = traceShow x () in ()

dbgAssert :: Bool -> a -> a
dbgAssert False !x = error "assertion failed!"
dbgAssert True !x = x

#else
dbg :: Show a => a -> ()
dbg _ = ()

dbgAssert :: Bool -> a -> a
dbgAssert = flip const
#endif

2. 再帰関数を {-# INLINE #-} すると遅くなった (~200 ms)

同じく 典型 90 問 031 で、セグメント木の更新関数 (再帰部分) を INLINE すると TLE が増えました。謎ですが、末尾最適化が働かなくなるのかと予想しています。

再帰関数の {-# INLINE #-} は一旦削除しておきます。

{-# INLINE [1] #-} のように、適切なフェーズを指定すれば速くなるのかもです。

3. Vector の monadic stream が爆速だった (~400ms)

副作用目的の連番ループ ([0 .. n]) を高速化します。

元ネタ

cojna さんの stream 関数が良さげで気になっていました。これは本物のイテレータ!:

stream :: (Monad m) => Int -> Int -> MS.Stream m Int
stream !l !r = MS.Stream step l where { step x | x < r = return $ MS.Yield x (x + 1) | otherwise = return MS.Done; {-# INLINE [0] step #-}}
{-# INLINE [1] stream #-}

テンプレートへの取り込み

stream をフォーマットして inclusive range (l, r) 版にするとこんな感じです:

{-# INLINE [1] rangeMS #-}
rangeMS :: (Monad m) => Int -> Int -> MS.Stream m Int
rangeMS !l !r = MS.Stream step l
  where
    {-# INLINE [0] step #-}
    step x
      | x <= r = return $ MS.Yield x (x + 1)
      | otherwise = return MS.Done

作った range を食わせる for 文も用意しました:

{-# INLINE forMS #-}
forMS :: (Monad m) => MS.Stream m Int -> (Int -> m ()) -> m ()
forMS = flip MS.mapM_

これで以下の 3 ループは等しくなります:

  1. リスト
forM_ [1 .. pred nSets] $ \s -> do
  1. Unboxed vector
-- rangeVG !i !j = VG.enumFromN i (succ j - i)
VU.forM_ (rangeVG 1 (pred nSets)) \s -> do
  1. Stream
forMS (rangeMS 1 (pred nSets)) \s -> do

ガバベンチ

ABC 301 E の Bit DP における 3 重ループ (集合、遷移元頂点、遷移先頂点) の書き方を変えて提出しました:

  1. List (forM_ + [l .. r]): 2477 ms
  2. Vector (VU.forM_ + rangeVG): 2309 ms
  3. Stream (forMS + rangeMS): 2068 ms

やはり stream が圧倒的に速かったです。テンプレートも stream を使うよう変更しました。

やはり Rust と張り合えるような Haskell の次の言語に期待したい……!

toyboot4etoyboot4e

ガバベンチ 4: repM_

単なる再帰関数でループが書けることに気付きました:

{-# INLINE repM_ #-}
repM_ :: Monad m => Int -> Int -> (Int -> m ()) -> m ()
repM_ !l !r !act = inner l
  where
    inner !i
      | i > r = return ()
      | otherwise = do
        act i
        inner (succ i)
  1. ループ (repM_): 2047 ms

衝撃の結果です。まさか生の vector stream を触る意味がなかった……? 随所で比較してみたいと思います。

toyboot4etoyboot4e

初歩的な回転の復習

人は皆、二次方程式の解の公式を忘れ、平方完成するようになる。信じられないかもしれないが本当の話だ。遠くない内に君も経験することになる。

マンハッタン距離の典型問題で 45 度回転が出てきました。回転、回転……? 回転といえば複素数、思い出さねば……!

関数のべき級数展開

無限回微分できる関数 fx = x_0 の周りで展開するとこうなるとか:

\begin{align} f(x) &= \sum \limits_{n=0}^{\infty} \frac {f^{(n)}(x_0)} {n!} (x - x_0)^{n} \bigg|_{x_0 = 0} \\ &= \sum \limits_{n=0}^{\infty} \frac {f^{(n)}(0)} {n!} x^{n} \end{align}

オイラーの公式

\sin, \cos をマクローリン展開して上手に処理すると e^{i\theta} = \cos \theta + i \sin \theta が求まったはず。

回転

r e^{i\theta} で表される点を、いや極座標表示する意味は無かったが、原点を中心に \phi 度回転させると:

\begin{align} r e^{i\theta} \cdot e^{i \phi} &= (x + i y) (\cos \phi + i \sin \phi) \\ &= (x \cos \phi - y \sin \phi) + i (x \sin \phi + y \cos \phi) \\ &= \begin{bmatrix} \cos \phi && -\sin \phi \\ \sin \phi && \cos \phi \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} \end{align}

特に \phi = \frac {\pi} {4} の場合、回転行列は:

\frac {1} {\sqrt 2} \begin{bmatrix} 1 && -1 \\ 1 && 1 \end{bmatrix}

基底を元に回転行列を求めるやつ

忘れた (そもそも線形代数が正しくインプットされていない orz)

マンハッタン距離と 45 度回転

2 点間の距離を d = |\Delta x| + |\Delta y| と定義するやつですが、 45 度回転すると d = \max (|\Delta x|, |\Delta y|) になって素直に求めやすくなります。思いつかなかった……。

45 度回転は、上の回転行列を \sqrt 2 倍すれば、 x, y 座標の足し算・引き算で処理できます。しかも \sqrt 2 倍に拡大したことで、回転後の座標系の \max (|\Delta x|, |\Delta y|) が元の座標系のマンハッタン距離と一致するトリビア付き。

感想

正しくインプットできなかった数学は、改めて学び直そうかと思いました。 FFT も典型らしい……!

toyboot4etoyboot4e

グリッド上の 01-BFS or 枝刈り BFS

問題

グリッド上の始点から終点へ駒 (例: 飛車) を動かす。始点から終点まで移動する最小の行動回数を求めよ。

方法 (01-BFS or 枝刈り BFS)

普通にグラフ探索を書いても通りますが、重複除去が難しく TLE しがちです。 01-BFS を使うと安定します。

参考になるのは 典型 043 の解答コードけんちょんさんの ABC 246 E 解説 など。とくにけんちょんさんの解説が良さそうです。

僕は重複した探索の除去を頑張っていましたが、そうかスコアで枝刈りすればよいのか……?

問題例

コードメモ

Python の倍くらいのコードになって非常に厳しい……

01-BFS

https://atcoder.jp/contests/abc246/submissions/41424816

bfs246E :: (Int, Int) -> UArray (Int, Int) Bool -> UArray (Int, Int, Int) Int
bfs246E !start !isBlock = runSTUArray $ do
  -- dp ! (y, x, iDir). The third dimension is required!
  !dp <- newArray ((0, 0, 0), (pred h, pred w, pred 4)) undef
 
  forM_ [0 .. 3] $ \iDir -> do
    writeArray dp (fst start, snd start, iDir) 0
 
  let popLoop Seq.Empty = return ()
      popLoop (((!y0, !x0, !iDir0), d0) Seq.:<| seq0) =
        foldM step seq0 [0 .. 3] >>= popLoop
        where
          -- collects neighbors
          step !acc !iDir
            | not (inRange bounds_ (y, x)) || isBlock ! (y, x) = return acc
            | otherwise = do
                !lastD <- readArray dp (y, x, iDir)
                -- REMARK: we can come to the same point in the same direction in different ways:
                if lastD /= undef && lastD <= d'
                  then return acc
                  else do
                    writeArray dp (y, x, iDir) d'
                    if iDir == iDir0
                      then return $ nextItem Seq.<| acc
                      else return $ acc Seq.|> nextItem
            where
              (!y, !x) = add2 (y0, x0) (dyxs VU.! iDir)
              !d'
                | iDir == iDir0 = d0
                | otherwise = succ d0
              !nextItem = ((y, x, iDir), d')
 
  popLoop . Seq.fromList $ map (\iDir -> ((fst start, snd start, iDir), 0)) [0 .. 3]
  return dp
  where
    !undef = -1 :: Int
    !bounds_ = bounds isBlock
    (!h, !w) = both succ . snd $ bounds isBlock
    !dyxs = VU.fromList $ [(1, 1), (-1, 1), (1, -1), (-1, -1)]

TODO: 枝刈り BFS

toyboot4etoyboot4e

作用付き モノイド (m, *) (operator monoid)

注: 群は初見です。意図せぬ誤りが含まれると思いますが、ご了承ください。

半群とは、モノイドとは

集合に結合的な演算 <> を与えたものを半群と呼びます。たとえば (Integer, max) の組 Max を半群のインスタンスにできます。半群の型クラスは以下です:

class Semigroup s where
  -- (s1 <> s2) <> s3 == s1 <> (s2 <> s3)
  (<>) :: s -> s -> s

半群に <> に関する単位元を加えたものを モノイド と呼びます。たとえば (Int, (+), 0) の組 Sum をモノイドのインスタンスにできます。モノイドの型クラスは以下です:

class Semigroup s => Monoid mwhere
  -- s1 <> m == m <> s2 == m
  mempty :: m

作用付きモノイド (m, *)

モノイド m に以下の性質を満たす作用 * :: m -> a -> a を加えた (m, *) を作用付きモノイドと呼びます。

m_2 * (m_1 * a) = (m_2 <> m_1) * a

作用付きモノイドの観点で見ると、 (m, <>, mempty, a, *) がすべて視野に収まります。

この式は (結合法則でもないし) 何かと聞いてみたところ、作用付きモノイドを教えていただけました。感謝!

例: 置換配列は作用付きモノイド

たとえば、整数の置換を表す配列は作用付きモノイドであるとみなすことができます。

  • モノイド m: 置換配列です。
    • 結合的演算 <>: 置換配列の合成を行います。
    • 単位元 mepmty: 恒等置換を表す配列 [0, 1, 2, ..] を返します。 [1]
  • 作用の対象 a: 置換される整数 Int です。
  • 作用 *: * :: 置換配列 -> Int -> Int は置換を実行します。
    作用は置換配列を置換関数 Int -> Int に変えるものであるとも捉えられます。

ダブリング (binary lifting)

作用付きモノイドはダブリング (binary lifting) の対象となります。ダブリングの際は作用付きモノイドを事前に結合できることを活かし、 m_1 同士の 2^i 回結合を事前計算しておきます。

ダブリング後は、 m_1n 回適用を定数時間で計算できます:

\begin{align} m_1 * m_1 * .. m_1 * a &= (m_1 <> m_1 <> .. m_1) * a \\ :&= (m_1)^{n} * a \end{align}

ここで (m_1)^{n} は事前計算しておいた (m_1)^{2^i} の結合によって計算できるため、 (大きな) 定数時間で計算可能です。 (もしくは (m_1)^{2^i} による置換を順次適用すれば良いです。こっちの方が一般的な上に高速ですね)

なぜ作用付きモノイドなんてものを考えるのか

考察と実装が穴埋めゲームになって簡単になるためです。

なぜ作用付きの半群ではなく作用付きモノイドを考えるのか

なぜ mempty が必要なのかという点ですが、いや別に無くても良いかもですね……。 m_1 の 0 回適用は、 n == 0 の場合で分岐すれば単位元は必要ありません。

ただダブリング以外の例では、分岐が減ると嬉しい場合が多いです。 mempty を遠慮なく畳み込み (結合 <>) の初期値として使えます。モノイドである必要は無いかもしれませんが、モノイドとした方が便利であるため、モノイドに限定して考えています。

どうしても半群が モノイド にならない場合、 Maybe で単位元 Nothing を加えれば半群がモノイドになります。分岐を外出しできて便利なので、やはりモノイドだけを考えれば良いのだ!

Haskell で作用付き モノイド を表現するには

1. 型クラスを使う

やはり class OperatorMonoid を作るのが最も美しい方法に思えます。 Maybe でラップすると半群がモノイドになる、モノイドのタプルはモノイドの直積であるなど強力な機能を利用できます。しかしモノイドへの慣れが必要なので、一旦置いておこうと思います。

2. データ型の中に関数を入れる

datanewtype を使っても良さそうです [2] 。まずこちらの実装を試し、慣れてきたら型クラスに移行してみようと思います。

こんな感じにしてみようかなと
data OperatorMonoid m a = OperatorMonoid m (m -> m -> m) (a -> m -> a)

-- 作用は m -> a -> a ではなく a -> m -> a の形にしました。
-- m -> a -> a の方が数学的に美しいですが、畳み込みの際は不便だと思います。

data BinaryLifting m a v = BinaryLifting (OperatorMonoid m a) (v m)

-- TODO: 使ってみる

binLift :: (VG.Vector v m, VG.Vector v Int) => OperatorMonoid m a -> BinaryLifting m a v
binLift opMonoid@(OperatorMonoid !op0 !mapp !_) = BinaryLifting opMonoid ops
  where
    step !op !_ = op `mapp` op
    !ops = VG.scanl' step op0 $ VG.enumFromN (0 :: Int) 62

binLiftReplacement :: VU.Vector Int -> BinaryLifting (VU.Vector Int) Int V.Vector
binLiftReplacement !op0 =
  binLift $ OperatorMonoid op0 (\op1 op2 -> VU.map (op2 VU.!) op1) (flip (VU.!))

-- | Binarily lifted operator monoid action application.
mactB :: (VG.Vector v m) => (BinaryLifting m a v) -> a -> Int -> a
mactB (BinaryLifting (OperatorMonoid !_ !_ !mact) !ops) !acc0 !nAct = VU.foldl' step acc0 (rangeVG 0 62)
  where
    step !acc !nBit
      | testBit nAct nBit = acc `mact` (ops VG.! nBit)
      | otherwise = acc

他の作用つきモノイドの例

  • 全方位木 DP
    作用の型が m -> m -> m だと思うのですが、正しい説明では無い気もして省略しました。

  • 遅延セグメント木
    実装中..

確認中……

もう少し整理できそうなので色々あたってみたいです。

https://zenn.dev/santamn/articles/81f4bf9a4cb139

脚注
  1. 恒等な置換を表す配列 [0, 1, 2, ..] の生成には、長さの引数が必要です。そのため置換配列をモノイドのインスタンスにすることは (通常) できません。ただし配列の長さが型パラメータで分かっている場合は mempty を定義できます。 ↩︎

  2. 型クラスを使用せずに関数 (の組) を渡すことを dictionary passing と呼ぶそうです。 ↩︎

toyboot4etoyboot4e

1 年前の僕「今、何を……!?」

今日の僕「何って、初歩的な Haskell をやっただけだが」 (※ 実際そう)

toyboot4etoyboot4e

AtCoder 以外のジャッジ環境

他サイトを覗いてみました。使用可能ライブラリの一覧すら見つからなくて辛い。ひとまず AtCoder におけるテンプレートを提出してみると……

  • Yukicoder: primitive が無い、 extra が無い
    要望を出せば通りそうな気はしています。
  • Codeforces: vector すら使用不能、コード長に制限あり
    C++ 前提という感じがしますね。
  • Topcoder: 実名前提なので登録したくない
    もはや登録したくない。

結局富豪スタイルで挑戦できるのは AtCoder だけでした。その AtCoder でもジャッジ環境の更新は隔年であり、競プロって凄くローカルなものだなと認識を改めました。

そもそもファイル分割ができない特殊な環境ですし、今から Common Lisp のライブラリを大量に入れようと思っても不可能ですものね。

Codeforces を覗いた後、 AtCoder の窓口は随分と広かったんだなと感激しています。 Haskell のジャッジ環境も奇跡的に良くできていて、先人に感謝するばかりです。今の AtCoder が無ければ、一生 Haskell を書く機会が無かったかもしれません。

toyboot4etoyboot4e

遅延評価セグメント木を実装してみた

PAST 上級本cojna/iota を参考に遅延評価セグメント木を実装しました。 cojna さんのコードは入出力やグラフを始め、全体的に理想的な構成に見えます。今後も参考にしていきたいです。

典型 90 問: 029 - Long Bricks(★5)

これを通しました。 とにかく大変でした 。色々な自信を失います…… (定期) 。今後リファクタリングしていきます。

https://atcoder.jp/contests/typical90/submissions/41609860

現在の疑問点

  • バグは無いのか
    PAST 上級本の類題などで検証します。

  • 正格なセグメント木と lazy なセグメント木のデータ型は分けるべきなのか
    分けない理由があるとしたら、正格版セグメント木の 1 点更新のコードを再利用できること?

  • class MonoidAction はまだ早い
    僕のレベルだと、問題毎に MonoidAction のシンスタンスを作ることになります。汎用的なモノイドだけ定義しておき、モノイド作用は変数で表現した方が良い気がしてきました。

典型 029 用の作用付きモノイド
derivingUnbox
  "Height"
  [t|Height -> Int|]
  [|\(Height h) -> h|]
  [|\h -> Height h|]

instance Semigroup Height where
  h1 <> h2 = max h1 h2

instance Monoid Height where
  mempty = Height 0
  mconcat = maximum

instance MonoidAction Height Height where
  mact = max

やはり Blender をやらなければならない

遅延評価セグメント木では、区間の移動時にプラットフォーマーゲームのグリッチみたいな変な挙動があって面白かったです (隣の区間へ壁抜けした後に親区間へ移動する) 。やはりこれを (無駄に) 3D モデルで共有してみたい……!

toyboot4etoyboot4e

遅延セグ木において『半群準同型性』が具体的に要求していること

葉への作用と区間への作用が同じモノイドによる作用で等しく表せる (ように値に文脈を埋め込む) こと

(葉に作用させてから結合させた値と、葉同士を結合させてから作用させた値が等しいこと)

toyboot4etoyboot4e

LCA を経由して、木の上の任意の 2 点間を畳み込む (構想のみ)

本日のモノイドはー、 (ドゥルルルルルルッルルルン) 可換モノイドです!

LCA (最小共通祖先) 高速計算法 (ダブリング + 2 分探索) の復習

共通祖先とは (例)

以下の木において、頂点 root, 頂点 child は 2 頂点 (v1, v2) の共通祖先です。特に child は、 (v1, v2) の最小共通祖先です:

      . root: common ancestor
      |
      . child: lowest common ancestor
     /  \
V1 .      .             # 見栄えしない
            . v2        # やはり Blender をやらなければ……!

木の上の任意の 2 点間の経路は、 v1 - lca(v1, v2) - v2 という 2 つの経路で構成できます。

lca(v1, v2)v1, v2 のどちらかと等しい場合は、片方の経路が長さ 0 であると言えます。

LCA 計算の事前準備

木の根から DFS して 2 つの配列を作成します:

  • 配列 1: 頂点 → 根からの距離
  • 配列 2: 頂点 → Maybe 親

配列 2. をダブリングすると、『頂点 vn 番目の親』を O(1) で計算できるようになります。

LCA を求める

  1. (v1, v2) を同じ深さまで移動させて (v1', v2') とする
  2. 2 分探索で LCA を求める
    判定問題 parentN d v1' == parentN d v2'd に関して単調です
      . root
      |
      . # 長い木の LCA も O(log N) で求まる
      |
~~~~~~~~~~~~~~~~
     |  |
     |  |
     |  |
     /  \
V1 .      . v2'
            . v2

LCA を経由した畳み込みを行うには

本題の畳み込みですが、事前準備でもう 1 つ配列を作成します:

  • 配列 3: 頂点 → 親へ移動する際に結合するモノイド
    ダブリングします

畳み込みのシグネチャとしては、おそらくこうなるはずです: けっこう難しい

実装しよう…… (・ω

toyboot4etoyboot4e

実装してみた (ABC 235 E)

モノイドをダブリングする時に、頂点の移動も追う必要があって苦戦しました。モナドか何かで既存の lca にモノイドの計算を差し込めたら良いのですが。

スピード面

1/2 の確率で TLE になります。遅すぎて使えない。。 フレンズさんは 617ms で通している ので、もう少し上手く実装したいです。

toyboot4etoyboot4e

cabal でビルドするとテストが爆速になった

今更ですが QoL が上がりました。アルゴリズムのオーダーが間違っていたらすぐに気付けそうです:

$ cabal build a-exe
$ cabal run a-exe

なぜ stack script を使っていたのか

stack でビルドすると、 a-exe, b-exe, .. がすべてビルドされ、 1 つのファイルにエラーがあるとテストが実行できなくなりました。

cabal だとそんな心配は無さそうですね。謎……

TODO: haddock

cabal haddock a-exe とかやっても html が生成される気配が無く……?

toyboot4etoyboot4e

PAST 対策中

2023/06/03 (土) 13:00 ~ 18:00 にアルゴリズム実技検定があります。上級認定を目指すなら、 15 問中 12 問解けねばなりません。もう遅いですが対策します。

https://past.atcoder.jp/

PAST 第 8 回 をやってみて

PAST の出題傾向は狭く深く。 12 問までは解説 AC できました。

  • 数列に関する問題が多い
    ソート、 2 分探索、尺取り法、座標圧縮、セグ木、主客転倒 (contribution の和) でゴリ押しすると解けます。方針通り 1 発で実装できるよう、ライブラリ化しておくことが重要です。
  • 2 部マッチングが 1 問
  • 考察が難し過ぎる問題が 2 問 (エキスパートは到底無理だ)

PAST 上級本

解きたい問題は 4 章・ 7 章で、解くべき問題は 5 章・ 8 章・ 9 章です。毎日 1 章は無理だな……

  • 1. 2 分探索
    平均最大化、 n 番目に大きい平均値などはやっておくと良さそうです。中央値の問題は難し過ぎたので出ない気も。
  • 2. 動的計画法
    ほぼ EDPC, TDPC の解説です。 PAST 過去問はあまり載っていませんでした。 PAST の出題傾向的にあまり解かなくて良い気がしています。 EDPC をわりと解いているしヨシ!
  • 3. 頻出テクニック
    出そう。列挙で TLE したら参照します。考察激ムズ問題は解けなくて良い気もします。
  • 4. 頻出アルゴリズム・データ構造
    Union-Find と LCA. 出てきたら美味しいですね。解いておきたい。
  • 5. ネットワークフロー
    二部マッチングとか最小費用流とか、ど典型が出題されるけど解けないやつ! 一番やるべき章ですね。後はやる気が……
  • 6. セグメント木
    遅延セグ木を実装しましたが、 これ過去問では出てないですね? 13 回 PAST O に出てる……
  • 7. セグメント木 + DP
  • 8. 平面走査 [エキスパート編]
    何も分からない。優先度は低めかも。
  • 9. 難問にチャレンジ! [エキスパート編]
    見てません。既知の手法の組み合わせ?
toyboot4etoyboot4e

PAST 第 13 回 が難しすぎる

上級は無理そうです。中級を目指すことにします。

WA 解消策

どうしても WA が取れなかった 3 問を upsolve して行きます。下手すると中級も怪しい……?

  • F: 切り上げ計算を間違えていた
    → 終わりだ……。少しでも怪しい計算は書き直して提出してみます。
  • J: 陰関数表示における点と直線の位置関係の判定式を解説から持ってきたら AC になった
    → 可能な限り式はネットからパクります
  • K: 解答の上限が問題分で与えられていた
    → 詰まったら問題分を読み直し、誤読が無いか確かめます

M, N, O の 3 問

  • M
    解説: 主客転倒で各頂点からの寄与を求める 。頂点 1 から各頂点に到達するために必要な (min, max) 頂点番号を調べることで求まる。なるほど……! そして方針が分かっていても、細部の詰めが甘く AC が遠い。この辺は完全に受験数学の域なんですよ。

  • N
    Mo's algorithm, あるいは平面走査というやつですか。未履修なのでキツい!

  • O
    解説: 遅延セグ木で上手く演算子を考える。ムズいって……!

感想

三ヶ月後なら全問解けそうですが、今ジャイアントキリングを狙うのは無謀です。追加で 2 回分くらい PAST を解いて、暇だったら M ~ O を復習します。

toyboot4etoyboot4e

PAST 第 12 回メモ

本番に見返す可能性もあるのでどんどん更新します。

WA の upsolve

  • F: 愚直解で解けますが、うかつな Haskell では TLE しがちです。 序盤でそんなに難しい要求は無い というメタ読みで定数倍の高速化に走りましょう。

WIP

  • G: Bit 全探索 (そういえば基本だ!)
  • H: ナップサック DP の亜種です。銅貨 0 枚のパターンを見逃して WA が出ました。なるほど。ここで折り返し地点とは、ヤバいな PAST ……。上級狙いなら 2/3 まで完了です。
  • I: これこそ B くらいの難度では……。
  • J: PAST って必ず幾何も出すよね! PAST 13 回の反省を踏まえ、円と長方形が重なった部分の面積でググりますが……何も出ない。交点の数で場合分けするんでしょうか……。飛ばそう。
  • K: Blackout 2 よりも難しいぞ?! 動的辺を特別に管理したが WA.
    • リストって FILO だった (実装ミス)
    • 初期値を見直し (ロジックミス)
    • 動的辺の判定ミス (実装ミス (変数名)) を修正して AC. まあ解けるけど時間が……!
    • 解説: 別に毎回 Union-Find を構築しなおして良い。上手く解きすぎたか……!
  • L: そういえばご飯を食べてない……。見通し立たず
  • M: 遷移元・遷移先が複数あるように見せかけて、左からピッチリ埋めていくだけの普通の DP です。 PAST 的にはセグ木 DP となるのか。
  • [N]: 行も列も O(1) で回転できるような 2 次元グリッドの持ち方? Seq 列じゃダメだ……
  • [O]: この記事 を参考に全探索して失敗?

Upsolve

  • J: 解説: 地道に算数を頑張る。そうか……
  • L: やはり DP らしいが……? → 総和の mod を固定すればナップサック DP になる。なるほど。
    • 試験対策としては、きっと ナップサック DP とセグ木 DP しか出ない んだから、どちらかに当てはめて試行錯誤すれば良かろうなのだ!
    • ナップサック DP は初期状態からの遷移に対する番兵が重要……が、 WA ……!
    • q の変域が [1, n + 1] であることに注意して AC (PAST は WA を狙ってくるな)
  • N: 解説: h \le w となるよう転置すれば十分間に合う
    • 制約 H \cdot W \le 2 \cdot 10^5 はそうやって使うのか……!
    • しかし TLE? 定数倍の高速化ではビクともしません。 Seqlog がデカいせいなのかVM.MVector (VUM.MVector Char) をリングバッファにするしか無いかもです。
  • O: 解説: 2 次元 FFT 。 FFT をやらねば!

感想

時間があっても 10 or 11 問が限界でした。粘って N を通せばあるいは。。

toyboot4etoyboot4e

PAST の問題傾向がかなり好み

PAST は基礎練に良いですね。 PAST をやれば、 ABC の D までは全く苦戦しなくなる予感があります。それは早解きに繋がりますし、高難度問題に挑めば解ける問題の上限も伸びます。総じて筋肉質な実戦向きの問題セットです。

  • 細部まで詰めて考えないと、すぐ WA になる
    序盤からかなり歯応えがあって頭を使います。高山トレーニング?
  • 出題範囲が狭い
    PAST の範囲を得意分野として固められます。
  • 高難度の問題もちゃんと出る
    水 diff 中盤級の考察・実装や上級アルゴリズムも出ます。

鉄則本、典型 90 とか EDPC のような問題セットは、限界を広げるには良いですが、実戦のパフォーマンスに直結しにくい面がありました。その成分は PAST 過去問で補完していきたいと思います。

toyboot4etoyboot4e

受験結果

64 点で中級でした。規約上怖いので感想はナシで!

順位表、解説、人の提出も読めなくて悲しい……!

間違えても解答をコミットできないように、 *past* を .gitignore に追記しました。 PAST でも oj/acc はそのまま使えます。

toyboot4etoyboot4e

競プロには意外と『コツ』が存在する

過去の自分 (伸び悩む競プロ初心者) には、案外『じゃあまず 100 問解いて!』以外のアドバイスがあり得る気がします。たとえば今の僕が知っているコツとしては:

Haskell が教えてくれたこと
  • モノイド、作用付きモノイド、半群準同型性で整理できるアルゴリズムは多い
    • ダブリング
    • セグメント木、遅延セグメント木
    • 全方位木 DP
  • グラフ (隣接リスト表現) を Vertex -> [Vertex] だと捉えると、幅広く BFS / DFS を考えられる
    • 遷移先の頂点は動的に取り寄せて良い (ABC 304 C, ABC 297 E)
    • 同様に、各桁が 0, 1, 2, 3 のいずれかで構成される n 桁の整数 (リスト) の生成も、遷移先が (桁数 +) 0, 1, 2, 3 のいずれかである探索と言える

コツは派生して多くの知識や解法に繋がります。逆に『コツ』を掴めていない分野を列挙すると、狭く具体的になりがちです。コツを掴みたいですね……!

苦手〜解きづらい問題例
  • 商列挙とか半分全列挙のような効率的な数え上げ
  • 超頂点を用意して辺の数を減らすパターン
  • ゴールから考えるタイプの動的計画法

競プロに限らず、たとえば Haskell においても『1 つの式の中にすべての文脈が載っているため理解しやすい』などの発見はあったはずですが、『コツ』言えるほど強力になることはまずありません。結局『じゃあまず 100 問解いて!』が最適なのかもしれません。

まず 100 問解くためのコツってありますか……? (血眼) 習慣にはニワトリと卵問題がッ

toyboot4etoyboot4e

また 1 つ手続き型プログラミングが上手くなってしまった

※ ただし Haskell に限る

具体例

一周回って 呼吸するように Haskell で手続きが書ける ようになってしまいました。喜んでいいんでしょうかw

今回は PAST #14: L で手続き型ループを書きました。

PAST #14 L の提出 (main のみ)
main :: IO ()
main = do
  [nVerts, nEdges] <- ints

  !outDegs <- VUM.replicate nVerts (0 :: Int)

  -- 1. do ブロックの中でやりたい放題:
  !edges <- replicateM nEdges $ do
    (!v1, !v2) <- both pred <$> ints2
    VUM.modify outDegs succ v2
    return (v1, v2)

  -- 2. `traceShow` でデバッグ出力:
  let !_ = dbg ("edges", edges)

  let !graph = accumArray @Array (flip (:)) [] (0, pred nVerts) edges

  -- 3. 気分はモナディック版 `until`:
  !result <- do
    !heap0 <- H.fromList <$> filterM (fmap (== 0) . VUM.read outDegs) [0 .. pred nVerts]
    let !_ = dbg ("heap", heap0)

    -- 4. `fix` 関数で再帰関数の定義・実行を同時に行う
    -- 5. `fmap` により `IO` モナドの中身をポイントフリースタイルで操作
    fmap (reverse . fst) . flip fix ([], heap0) $ \loop (!acc, heap) -> case H.uncons heap of
      Nothing -> return (acc, heap)
      Just (!v, !heap') -> do
        let !vs = graph ! v

        let !_ = dbg (v, vs)
        -- 6. `foldForM` で低インデントを保つ
        -- 7. `<=<` 演算子でモナディック関数合成
        loop <=< fmap (v : acc,) . foldForM heap' vs $ \heap'' v2 -> do
          VUM.modify outDegs pred v2
          !n <- VUM.read outDegs v2
          let !_ = dbg ("count", v2, n)
          return $
            if n == 0
              then H.insert v2 heap''
              else heap''

  putStrLn . unwords . map (show . succ) $ result

使用したテクニック

  1. do ブロックの中でやりたい放題
    辺の入力をパースしながら、次数のカウンタ (MVector) を書き換えています。

  2. traceShow でデバッグ出力
    スニペット定義して print 文のように traceShow を使っています。

  3. 気分はモナディック版 until
    until :: (a -> Bool) -> (a -> a) -> a -> a のうち、ステップ (a -> a) と初期値 a をモナディックにしたい! この場合、 do ブロックの中で初期値を作り、再帰関数でループしてしまいます。

  4. fix 関数で再帰関数の定義・実行を同時に行う
    ループをシュッと書く常套手段です。なぜこうなるのか知りませんが、不動点コンビネータ凄い。

  5. fmap により IO モナドの中身をポイントフリースタイルで操作
    (\x -> f <$> g x)(f <$>) . g または fmap f . g と書いてポイントフリーにできます。

  6. foldForM で低インデントを保つ
    foldForM a b f = foldM f a b としています。関数部分を最後の引数にしたおかげで、インデントの数を減らすことが出来ています。

  7. <=< 演算子でモナディック関数合成
    f >>= gg <=< f は等価です。とりわけ f が長い場合は、 g <=< f と書くと端的な表現になります。

それ Haskell を使っている意味あります???

だ、だって、ポイントフリースタイルで関数合成する喜びは他の言語で味わえないから……!

それ他の Haskller が読めます?

無理かも……

toyboot4etoyboot4e

逆に純粋関数型で書く意味はあるのか

ケースバイケースだと思うので、具体例を考えてみました。

  • ナップサック DP
    入力 (w, v) を受け取る度に、手持ちの状態を発展させます。畳み込みがメンタルモデルにハマっています。しかし for each ループもメンタルモデルと合致するのです。あえて畳み込みを好む意味はあるのでしょうか?

    • (For each ループ + 可変変数) vs 畳み込み
      For each ループでは、ループの外側で定義された可変変数を書き換えます。どの可変変数がループと結びついているかは、ループの中身を読まねば理解できません。
      畳み込みでは、状態を入力として受け取り、新しい状態に発展 [1] させます。スコープが明確であるという観点で、僕は畳み込みが優れていると思います。

    • ループと関連した可変変数が一目でわかる場合
      しかしサブルーチンを分けてスコープを区切った場合、以下の for ループは明らかに result 配列への副作用を狙っていることが読み取れます。入力 (w, v) を処理する毎に状態を書き換えるループ、これは畳み込みと等価なメンタルモデルで、甲乙つけ難いと思います。

      fn solveKnapsack(maxW: usize, wvs: impl Iteartor<Item = (u64, u64)>) -> Vector<u64> {
          // w -> v
          let mut result = vec![u64::MAX; maxW + 1];
          result[0] = 0;
      
          for (w, v) in wvs {
              // ~~
          }
      
          result
      }
      

      ナップサック問題に関しては、大した差が無さそうです。上のループは可変変数を使った畳み込みと言えなくもありません。

  • グラフ探索の際、訪問済み頂点を MVector で管理すべきか IntSet で管理すべきか
    IntSet の意義は、 sparse であることと immutable であることです。グラフの頂点は大抵密なので、 sparse である意味はありません。また過去のスナップショットを見返すことが無いため、 immutable データ構造である必要もありません。総じて IntSet を使うメリットが無いと捉えています (好むべき理由も探したい [2] ですが) 。
    MVector を使うと、時間を進めた (状態を書き換えた) 際にスナップショットが残りません。また再帰した際にも常に同じ状態を共有する (時間が一方向に進む) ことが明示的になります。よって MVector をキャプチャしたクロージャの中で再帰探索するのが理にかなっていると感じます。
    グラフ探索は手続き的な表現が良く、さらにモナドで意図が明確になると思います。手続き型言語 Haskell 最高!

  • 手続き的 or mutable な解法しか思いつかない問題を純粋関数型で解く
    純粋関数型的手法で問題が解けると、多くの発見があって意義深いのですが、理解が難し過ぎるという問題があります。そのため手続き的な解法しか無いと仮定してしまいます。

    • 手続きを純粋関数型言語で書く利点
      利点は無いと思います。ポイントフリースタイル関数合成くらいしか思いつきません。他のメリットを享受できるなら、ここで突出できなくても良いのではないでしょうか。手続きを Haskell で書く意味は無いが、他の加点を考えて総合はプラス! ポジショントークができました。
脚注
  1. 状態発展は自分用語ですが、量子力学で聞いた言葉でした。 ↩︎

  2. IntSet で苦労した経験から、結論ありきで考えてしまった気がします。たとえば IntSet + State モナドならセンスが良いと思います。パフォーマンスは犠牲になりますが…… ↩︎

toyboot4etoyboot4e

AC 503 ボルト

記念用・観賞用です。

とか言って見返したことは無いですね……

AtCoder Problems のスクリーンショット


ABC 305 で 503 AC に到達


水色になりた過ぎる来歴


水色になりた過ぎる来歴


PAST と常設コンが半分くらい


2019 年に Rust で ABS を挫折 (面白くなかったんだろうな……)


平日に問題を解いたらレーティングが上がった


2019 年に Rust で ABS を挫折 (面白くなかったんだろうな……)

toyboot4etoyboot4e

AHC 020 に参加してみた

AHC 初参加で水パフォ取れました。やったー!

ヒューリスティック・コンテストに関して

AHC 020 は初心者を意識したコンテストだったらしく、かなり ABC に寄せられています。制約は以下でした:

  • 制限時間は 4 時間 (15:00 ~ 19:00 の開催)
  • 提出のペナルティ無し、提出間隔は 5 分以上

使用したアルゴリズムは最小全域木と 2 分探索です。途中からスコアが頭打ちになり、 ABC とは違った壁を感じました。もう打つ手が無いぞ!

使用言語に関して

今回も AtCoder ガチ言語 Haskell で参加しました。 Tier 3 までの言語なら、大きな差は出ないように思いました。特にランダム性無しで解を求める際は時間制限を持て余す程です。

Common Lisp でも問題無く参加できそうですが、新たな病気に罹患するまでは Haskell 続投かなぁと思います。

今後

公式解説放送 を観たりサンダー本を読んだりしようと思います。今回は焼きなましだったらしいですが、適用方法が見えません。勉強します。

ABC をやらなければ一生 2 分探索しなかったでしょうし、 AHC をやらなければ一生焼きなめししないでしょう。雰囲気が分かる程度まではやってみようかと思います。

toyboot4etoyboot4e

木 DP 再び (典型 90 問)

部分技の畳み込み方法 (a -> [b] -> a vs foldl' + s -> a -> a)

典型 073公式解説 において、部分木の畳み込みが a -> [b] -> a の観点で述べられていました。これは containersfoldTree相当します (foldTree を使った人の提出) 。この考え方は難しいよ……!

ステップ処理の s -> a -> a で入力 (子要素) を 1 つずつ畳み込む形、いつものナップサック DP のように考えると簡単になりました。言語化が辛いので、ここに blender の図を貼りましょう。

これは Blender の図、の図……

Unbox の実装に関して

タプルの newtype を使うと、 derivingUnbox が簡単になる気がしました:

newtype Op = Op (Int, Int)
  deriving (Show, Eq)

-- タプルの中のデータ数が増えても、 `derivingUnbox` の書き方は同じ:
derivingUnbox "Op" [t|Op -> (Int, Int)|] [|\(Op !x) -> x|] [|\x -> Op x|]

タプルの操作も便利なことが多く、フィールドを持つよりも良さげな感が……

全方位木 DP 再び

foldTreefoldTreeAll のシグネチャを見直し、 典型 039 を再び AC しました (提出) 。

もう 10 回目を超えているのでサクッといけました。

main
newtype Acc = Acc (Int, Int)
  deriving (Show, Eq)
 
derivingUnbox "Acc" [t|Acc -> (Int, Int)|] [|\(Acc !x) -> x|] [|\x -> Acc x|]
 
newtype Op = Op (Int, Int)
  deriving (Show, Eq)
 
derivingUnbox "Op" [t|Op -> (Int, Int)|] [|\(Op !x) -> x|] [|\x -> Op x|]
 
instance Semigroup Op where
  (Op n1) <> (Op n2) = Op $ add2 n1 n2
 
instance Monoid Op where
  mempty = Op (0, 0)
 
instance SemigroupAction Op Acc where
  sact (Op (!n1, !d1)) (Acc (!n2, !d2)) = Acc (n1 + n2, n1 + d1 + d2)
 
instance MonoidAction Op Acc
 
main :: IO ()
main = do
  [nVerts] <- ints
  !edges <- replicateM (pred nVerts) (both pred <$> ints2)
 
  let !tree = accumArray @Array (flip (:)) [] (0, pred nVerts) $ concatMap2 (second swap . dupe) edges
  let !results = foldTreeAll tree (const (Acc (1, 0))) (\(Acc nd) -> Op nd)
 
  print $ (`div` 2) . VU.sum $ VU.map (\(Acc nd) -> snd nd) results

考察で全方位木 DP を引っ張り出してくるのが難しいなと思います。木において O(N) で答えを出そうと思えば、パタンマッチで全方位木 DP になるのですが、毎回自分の頭で解き方を考えるのが高難度帯では効いてくるそうで、難しいです。考えるってパタンマッチじゃないんですよね……?

toyboot4etoyboot4e

参考になった色変記事

巷の入水記事では、よく『(狭めの範囲) を勉強したら入水できました!』となっています。それがいかに凄いか身にしみて理解していますが、自分の参考にはならないのです……!

逆にどういう記事が参考になったかメモしておきます。

どの記事も面白い (から読んでいる) のですが、参考になるかは別の尺度ですよね。

ピックアップ

  • 【競プロ】新人SEがAtCoderを始めて水色になった【色変記事】
    プレゼンが上手いです。まずタイトルが目立ちますし、中身を見れば隙きの無い実力を得たことが伺えます。 全部解けるようになれってことか……!
    モチベーション面 で参考になっています。半年で水色、 1 年で青 というリアルタイム性が刺激的です。 Twitter も面白いし何もかも凄い。

  • AtCoder黄色を目指してやってきたこと
    色辺記事にしては珍しく、主に 考察やライブラリ整備 に関して言及されています。マメに考えて自分に適用できると役に立ちそうです。
    この方は解くのが遅いと言っていますが、 ABC で並走する限り、青色コーダーよりちょっと速いぐらいのスピードです。つまり僕からすれば爆速なのです。ギョギョーッ

  • RubyとCrystalでAtCoder青色になりました
    ライブラリ整備の具体例として、累積和が挙げられていました。 csum.sum(l..r) のようにアクセスするようです。僕は毎回図を書いて添字を考えていますが、その (認知的) 負荷が無くなる ということですね。上 ↑ の記事はこういう発見の重要性を伝えていたのかと非常に参考になりました。

感想

コツを見つけるのがコツだ!? そんな記事たちが刺激的でした。その他の記事も楽しく拝見しております。色辺記事を書いてくれた人の SNS は (忘れていなければ) 大体フォローしちゃう

toyboot4etoyboot4e

cojna/iota メモ

美麗…… d(•, •)

完璧過ぎて、自分でライブラリを書く意味が無い気さえしてきます!

読みました

Data.Buffer

長さ n or 2n のMVector のバッファと、その使用領域を表す [front, back) を持っています ([front, back) は長さ 2 の MVector で持っています) 。

バッファは stack / queue / dequeue として利用できます。ただしリングバッファではないため、 キューとして使用する場合は (たとえ popFront しても) n 回までしか pushBack できないはずです。

生の vector では pushBack できないため、 pushBack できるだけでも相当ありがたかったりします。 Seq をキューとして使うのも中々つらいし……

基本すべてのスロットが使い捨てであり、 push の最大回数を見積もって使うのが良さそうです。競プロ的ですね。

Data.Prelude

  • vector の stream 部分で爆速イテレーション
  • binarySearch .. = runIdentity $ binarySearchM ..
  • ByteStringBuilder 関連
    • なお ByteStringBuilderUnbox ではないため、 a -> ByteStringBuilder を受け取って Vector a を直接 fold します (map を挟みません) 。
  • 基数ソート関連? (使い所は? 速い?)

app/Main.hs

haskell-src-exts で関数定義を取り出して、 Main.hsappendFile する運用のようです。 AST を文字列に変換する際、それぞれの関数を 1 行に minify しています。

#define MOD のようなマクロ定義はパース前に取り除きます。ファイルごとに #define があるため重複するのと、マクロを消さないと Haskell としてパースできないようです (たぶん) 。

Data.Graph.Sparse

SparseGraph wCSR 形式 (隣接リストを 1 つの配列で済ませる形) のグラフです。重み付きグラフや木も同じデータ型で持つようです。

  • 重さを別の Vector で持ち、 adjadjW を両方提供する
  • Builer を切り出す
  • RecordWildCard 言語拡張で destructuring ができる
  • 添字は 1 次元限定
    2 次元グリッドなどで SparseGraph を使う場合は、手動で次元を変換しているようです。ここは盆栽の余地ありですね。

読みたいです

分かりません

  • Data.PrimParser
    モナディックパーサっぽいですが……? runSolver と組み合わせると、とにかく main がカッコいいです。羨ましい。
toyboot4etoyboot4e

いつか Haskeller に聞きたいこと

溜まったら特攻して聞きに行きます。

  1. ソースのバンドリングの方法
    cargo-equip 相当のツールを持っている? (つまりライブラリのソースをマージして Main.hs を生成している?) この際に minify したり IL(..) を自動で付けている?

  2. 可変長 vector
    標準的なライブラリが無いと思うが、自作している?

  3. 1 次元 vector への多次元 view
    これはソースを読めば分かるはずですが、どれだったかな……?

  4. 考えています……

toyboot4etoyboot4e

Main.hs の bundler を作る (1): 構想

自作ライブラリ (stack/cabal プロジェクト) からファイルを選び、適宜 Main.hs に合成できるようにします。要は cargo-equip の Haskell 版、自分専用の cojna/iota/app/Main.hs を作ります。 ~200 行くらいの軽いコードになるはずです。

動機

HLS を動かしたいです。大きなソースで Template Haskell を使うと HLS が動かなくなり (泣), 変数の一括リネーム等が不可能になります。必要なコードのみ Main.hs に取り込む形にすれば、 HLS も動くようになるはずです。

先駆者の動き

運用方法 (想像)

  1. My.Prelude のみをバンドリングした Main.hs をテンプレートとします。
  2. 随時 iota Data.Algorithm.BinarySearch a/Main.hs のようにライブラリを追加していきます。

ソース構成

先駆者の Main.hs は以下の並びです:

  1. 言語拡張
  2. Import
  3. main 関数
  4. 合成されたライブラリのコード

実装

言語拡張・ Import は、テンプレート内ですべて列挙すれば良いです。ライブラリ中のファイルを Main.hs に合成する際は、言語拡張・ Import を取り除いた上で Main.hs の末尾に追記できれば良いです。

haskell-src-exts を使うと、 Haskell のソースファイルの AST が得られます。これで関数やデータ型の宣言のみを print したり、コードの prettify/minify をするようです。

追加でやりたいこと

ライブラリ部分を 1 行に minify する

なんかできそうなんですよね……。 AST を 1 行に minify したやつを ; で繋げて行きます。これでいいんじゃないかという気がしてきました。

f :: Int -> Int ; f a = a + 3 ; g :: Int -> Int ; g x = x + 4 ;

Main.hs をブロック分けしてソートする

Main.hs のパーツごとにヘッダを作り、パーツの順番を操りたいと思います。

  • main 関数をファイル末尾へ持っていく

    • 僕含む Vimium ユーザが、 G 一発で main を発見できるのがメリットです。
    • Main.hs が壊れている (パースに失敗する) 場合も動作して欲しいと思います。
      やはり AST ではなくテキストとしてパーツをパースするのが良さそうです。
  • 取り込まれたライブラリをアルファベット順でソートする

    • たとえば MyLib.Algorithm.BinarySearchMyLib.Prelude よりも先に来るようにします。

CLI

  • テンプレート (全言語拡張・Import + Prelude) の自動生成
    • 全言語拡張・ Import は、ライブラリファイルの AST から抜き出したものの和です。
  • ファジーファインダで合成するファイルを選ぶ

やらない (けどやりたい) こと

依存関係の自動解決

  • AB に依存している場合、 A の合成時に B も自動で読み込む
  • もしくは合成ファイルのセットをファジーファインダから選ぶ

提出コードを最小化する

  • ソース提出前に、未使用のコード (関数・ Import) を削除する
    • AST 上で未使用コードを探るツールが無いか
    • cabal をライブラリとして使う
    • cabal (CLI) のエラー出力を使う
      cabal のエラー出力から未使用関数名を取り出す → AST から関数を削除する → cabal のエラー出力から未使用 Import を取り出す → 未使用 Import を削除する?

Bundling 化を意識させない

  • 自作ライブラリを直接 Import 可能とする
    Main.hs の中で import MyLib.Algorithm.BinarySearch と書いたら、提出時に自動で展開されます。もちろん Main.hs の編集中は、自作ライブラリのコードにジャンプしたりできます。
toyboot4etoyboot4e

Main.hs の bundler を作る (2): 1 行に minify してみる

toy-lib

  • stack new でプロジェクト作成
  • iotaMain.hs をパクって来て動かす
  • rawSystem でエラー。 NixOS のためか stack が見えていない。。。
    stack --no-nix-pure run .. で解決!!

あるいは stack.yaml で pure を切っておきます:

stack.yaml
# NixOS では Nix integration 有効がデフォルトらしいので、
# stack を見つけられるように `pure` を切っておく:
nix:
  pure: false

2,500 行のテンプレートを 1 行に minify してみる

やってみました。 AtCoder 上では見やすいですね:

https://atcoder.jp/contests/abs/submissions/42308246

エディタで長い行を表示すると重い重い。。畳んでサクサク動くようになりました。コード自体は相当長いため、相変わらず LSP はセグフォで死んでいます。

toyboot4etoyboot4e

toy-lib

ライブラリのリポジトリを作りました。 CLI でライブラリ全体を 1 行に minify します。

ファイル分割したおかげで、 haddock がモジュール分けされるようになりました:

https://toyboot4e.github.io/toy-lib/

こんな感じだったのか〜。

ただファイル冒頭の『GitHub の README を見てくれ!』というコメントすら、何を言いたかったのか分からない……w ← package.yamldescription デフォルト値でした。そこから取ってくるのか……

toyboot4etoyboot4e

トポロジカルソート

モジュールの依存関係でソートしなければコンパイルに失敗する

Done

toyboot4etoyboot4e

Unrated 対策下で常設コンにおける Haskell の提出を読むには

提出が多すぎて、クエリ処理が終わらないっぽいですね……!

  • 👍: ユーザで絞り込む (mod_poppo 氏!)
  • 👎: 言語で絞り込む (遅過ぎて弾かれる)
toyboot4etoyboot4e

制限が緩くなった気がする

EDPC における Haskell の提出もガンガン読めるようになっていました。ありがたや〜

toyboot4etoyboot4e

Haskell 同人誌が読みたい

妄想です。

勝手にこういう画像作っちゃダメですね……

ドリームキャストはともかく、実際 Haskell で茶色コーダーになるための本があればいいなと思います。ループ処理やリストで FIFO など、『当たり前』だけれど経験値の溜まりにくい部分を、スラスラ読み流すだけで補える本があれば……!

toyboot4etoyboot4e

グラフ典型メモ

やはりグラフが難しい。。水 diff まではパタンマッチで解けると思いたいです。

  • 無向グラフの閉路収集 (ABC266 F など)
    すべての頂点の次数を数える。次数 1 の頂点から順次取り除く。新たに次数 1 の頂点ができた際はキューに入れていく。残った頂点が閉路上の頂点である。

  • すべての盤面を頂点としたグラフを考える (ABC 224 D など)
    初期の盤面から最終の盤面までを BFS するなどして解く。盤面の遷移は Vertex -> [Vetex] と捉え、動的に構築して良い。特に盤面の数が多い時には、遷移可能な盤面のみがメモリに載って良い。

  • 超頂点を経由して遷移の本数を減らす (どれだっけ)
    特に全頂点間に辺が張られている場合、辺の数が N (N - 1) から N になって大幅に計算量が下がる。

  • 考察により負の辺を除去して Dijkstra (ABC 237 E)
    ダブルカウントの成分を辺の重みとする。その他はオフセットの差で求まる。

  • UnionFind 巻き戻しもどき (ABC 264 E, PAST のどれか)
    クエリを後ろから処理する/毎回 Union-Find を作り直すなど。

  • 探索状態の数だけグラフを複製する。グラフからグラフへの有向辺で状態遷移を表す。
    社長のツイート: https://twitter.com/chokudai/status/1551834978748489728

  • 01-BFS (典型 90 など)
    コスト 0 の遷移先が N 個ある場合、コスト 0 の遷移を N 回繰り返すと捉える。ただし探索時に状態を加味する。たとえばグリッドの場合、位置 * 方向を探索する空間とする。遷移先を集める際は、 Seq の前後に順次挿入する。遷移は Vertex -> [Vetex] と捉え、動的に構築して良い。

  • 全方位木 DP (典型 90 など)
    全頂点間の計算を O(N) で実施する。木の畳み込みが有効な場合のみ有効。

  • 徐々にネットワークを広げる (ABC 214 D)
    全頂点間の計算を O(N) で実施する。経由する 1 辺のみが結果を決める場合に有効……、、、

  • LCA

toyboot4etoyboot4e

包除原理 レベル 1

忘れそうなのでメモしておきます。

包除原理とは

すべての積集合を元に和集合を求めるやつらしいです。 tsutaji さんのスライド の『初級』の部分を参考にしています。

包除原理の使い方

\{\mathbb{k} \}_{\mathbb{k}}\{ n \le N \} \cap \mathbb {N} の空ではないすべての真部分集合の集合として [1] ……

\begin{align} | \bigcup_{i=1}^{N} {A_i} | &= \sum_{\mathbb{k}} {(-1)^{|\mathbb{k}|}} |A_{\mathbb{k}}| \\ A_{\mathbb{k}} &\coloneqq \bigcap_{i \in \mathbb{k}} {A_i} \end{align}
  • |\mathbb {k}| が奇数なら -, 偶数なら +
  • \mathbb k のイテレーション方法
    • ビット全探索
    • |\mathbb k| 毎にイテレートして、対称性を元に {}_N C_{|\mathbb k|} をかける

問題

脚注
  1. ベキ集合とか言ってシュッっと書きたいが…… ↩︎

toyboot4etoyboot4e

2D MVector, 爆速……!

ABC 307 CArrrayMVector に変えるだけで 763 ms の解答が 134 ms になりました。なんか知らんけどいいぞ……!

Data.Ix が遅いとか vector のストリームが速いとか?

2D MVectorへの view

MVectorslice すると 2D vector のように扱うことができます (元ネタ)。基本はこんな感じで行こうと思います。

-- | Creates a 2D view to a mutable vector.
{-# INLINE view2dVGM #-}
view2dVGM :: (VGM.MVector v a) => (Int, Int) -> v s a -> V.Vector (v s a)
view2dVGM (!h, !w) !vec = V.generate h (\i -> VGM.slice (w * i) w vec)

{-# INLINE read2d #-}
read2d :: (PrimMonad m, VGM.MVector v a) => V.Vector (v (PrimState m) a) -> (Int, Int) -> m a
read2d !vec (!y, !x) = VGM.read (vec V.! y) x

{-# INLINE write2d #-}
write2d :: (PrimMonad m, VGM.MVector v a) => V.Vector (v (PrimState m) a) -> (Int, Int) -> a -> m ()
write2d !vec (!y, !x) = VGM.write (vec V.! y) x

TODO: haddock test を書く、ここに貼る

toyboot4etoyboot4e

隣接行列 と行列累乗

概要

正方行列 A_{i,j} で有向グラフを表す (頂点 i から j へ辺が張られていれば 1, 張られていなければ 0 とする) 。 (A^k)_{i,j} は頂点 i から頂点 j への長さ k の経路の数を表す。

本当は walk の数らしい? 重さも載りそう

  • 対角成分は自己ループを表すため注意 (大抵 0 のはず)
  • 累乗計算はダブリングすると高速化できる

行列累乗 (mod) の計算方法

Array

mod_poppo 氏の提出 からパクりました。ただしaccumArray よりも listArray の方が速いです。

MVector

TODO

massive

TODO

対角化

TODO

問題

  • EDPC R - Walk
    頂点数 50 遷移回数 10^{18}
  • ABC 212 E
    頂点数 5000 でダブリングすると TLE になりました。 N^3 くらいかかりそうだし。思案中…… → 参りました、、!
toyboot4etoyboot4e

EDPC / TDPC の難度

EDPC をある程度やると、水 diff DP がサクっと解けることもあります。グラフの EDPC 、転倒数の EDPC というような高難度問題セットがあれば、同じように高山トレーニングできて良さそうですが。

https://twitter.com/kyopro_friends/status/1674365327285833728

案外黄 diff 過去問を解くのがレーティング向上の最短ルートだったりして……? そんなことを考えながらも、本日も解説 AC をキメて行きます。

toyboot4etoyboot4e

言ってる側から解けない DP が……

驕るには最後にします。半年間灰色レーティングだった日々を思い出せ…… orz

https://atcoder.jp/contests/ABC216/tasks/abc216_f

解き方メモ

任意の組み合わせが許される場合、 (全探索と言わず) 都合の良い順に貪欲に見ていけば処理できることもある

toyboot4etoyboot4e

どうしたことだ、この Haskell の提出の数は……!

増えているッ……! 提出の種類が、圧倒的にッ…………!!

Twitter が見れない時も、 Zenn が Twitter なので平常運転だったりします。コミュニティガイドライン的にはよろしくないですが……

toyboot4etoyboot4e

内部レーティング

AtCoder のレーティング計算式には『リセマラ補正』がかかっているそうです。

レート(第三段階)=レート(第二段階)−リセマラ対策補正

https://qiita.com/anqooqie/items/92005e337a0d2569bdbd#性質3-リセマラ対策

Type Checker で補正前のレーティングを確認できます。参加回数さえ増やせばとっくに色変してるじゃないか! という場合もあるようです。早々と色変する人は、 1 色上の実力を持っているかもしれません。

https://atcoder-type-checker.herokuapp.com/

toyboot4etoyboot4e

Stan (Haskell static analyzer)

久しぶりに Mac を開くと、 HLS から謎の lint が:

これは stan による提案 STAN-0210 のようです。

古い GHC では forM_ が遅くなりがち

改善方法としては、モナディックループには loop を使えとあります。いや〜使いにくい。

当面は cojna/iota の stream (vector パッケージの monadic stream) 相当の関数を使います。 関連の Issue は既に閉じており、最近の GHC では改善が期待できそうです。

Data.Ix.range に対する forM_ の高速化

Data.Ixvector ベースのグラフ実装のため、 range をイテレータで高速化したいと思います。まずは Data.Ix のソースコードを見てみます。

Int に対する実装
instance  Ix Int  where
    {-# INLINE range #-}
        -- The INLINE stops the build in the RHS from getting inlined,
        -- so that callers can fuse with the result of range
    range (m,n) = [m..n]

    {-# INLINE unsafeIndex #-}
    unsafeIndex (m,_n) i = i - m

    index b i | inRange b i =  unsafeIndex b i
              | otherwise   =  indexError b i "Int"

    {-# INLINE inRange #-}
    inRange (I# m,I# n) (I# i) =  m <=# i && i <=# n
(Ix, Ix) に対する実装
instance (Ix a, Ix b) => Ix (a, b) where -- as derived
    {-# SPECIALISE instance Ix (Int,Int) #-}

    {-# INLINE range #-}
    range ((l1,l2),(u1,u2)) =
      [ (i1,i2) | i1 <- range (l1,u1), i2 <- range (l2,u2) ]

    {-# INLINE unsafeIndex #-}
    unsafeIndex ((l1,l2),(u1,u2)) (i1,i2) =
      unsafeIndex (l1,u1) i1 * unsafeRangeSize (l2,u2) + unsafeIndex (l2,u2) i2

    {-# INLINE inRange #-}
    inRange ((l1,l2),(u1,u2)) (i1,i2) =
      inRange (l1,u1) i1 && inRange (l2,u2) i2

    -- Default method for inde
(Ix, Ix, Ix) に対する実装
instance  (Ix a1, Ix a2, Ix a3) => Ix (a1,a2,a3)  where
    {-# SPECIALISE instance Ix (Int,Int,Int) #-}

    range ((l1,l2,l3),(u1,u2,u3)) =
        [(i1,i2,i3) | i1 <- range (l1,u1),
                      i2 <- range (l2,u2),
                      i3 <- range (l3,u3)]

    unsafeIndex ((l1,l2,l3),(u1,u2,u3)) (i1,i2,i3) =
      unsafeIndex (l3,u3) i3 + unsafeRangeSize (l3,u3) * (
      unsafeIndex (l2,u2) i2 + unsafeRangeSize (l2,u2) * (
      unsafeIndex (l1,u1) i1))

    inRange ((l1,l2,l3),(u1,u2,u3)) (i1,i2,i3) =
      inRange (l1,u1) i1 && inRange (l2,u2) i2 &&
      inRange (l3,u3) i3

    -- Default method for index

リストの範囲構文やリスト内包表記を使った素直な実装でした。これらに替わりの手動でイテレータを実装していきます。

WIP

toyboot4etoyboot4e

高速化関係

Libary Checker (Yosupo Judge)

AtCoder Library Practice Contest の強いやつという感じです。これはいつか制覇してみたいですね。記事のネタにもなりそうですし。ジャッジの仕組みも気になります。


なんとサイト自体に AC 表示機能がある。できれば最速 AC 時間と自分の AC 時間も表示してほしい。

online-judge-tools/verification-helper

oj-verifyLibrary CheckerAizu Online Judge の問題をローカルや CI でジャッジするためのツールのようです。実は AtCoder の問題もジャッジできるようで、なんちゃってベンチマークに使ってみたいと思います。

{- verification-helper: PROBLEM https://onlinejudge.u-aizu.ac.jp/courses/lesson/2/ITP1/1/ITP1_1_A
 -}
-- ファイル冒頭のコメントで問題を指定できるっぽい

ACLにおける定数倍高速化

こたつがめ先生のスライドです。面白い内容がいっぱい。特にグラフを CSR 形式 で持つやつは最近僕も取り組んでいます。

toyboot4etoyboot4e

array モドキの n 次元 vector

cojna/iotaSparseGraph w は見通しが良いです。辺に重みがある場合、無い場合も同じデータ型を使用できます。アクセサが複数あり、重みが不要なら gr `adj` v, 重みが必要なら gr `adjW` v を使います。

作りたいもの

添字を Ix に拡張した SparseGraph i w を作ろうと思います。 内部的には、頂点は Int 型になるため常に Vector を使えば良いです 。ただしユーザ側の API では Ix 型を使いたいです。そのため Ix, Int 間の相互変換を提供する Unindex 型クラス、 array より高速な array モドキとして IxVector, IxMVector 型を作ろうと思います。

以前の投稿

Unindex

index の逆操作があると、 i, Int の相互変換ができてグラフの API に良さそうです。実装は適当、制約は強めでいいかな……:

Unindex.hs
class (Ix i, VU.Unbox i) => Unindex i where
  -- TODO: Fusing
  -- https://wiki.haskell.org/GHC/Using_rules
  unindex :: (i, i) -> Int -> i

instance Unindex Int where
  unindex _ !v = v

instance Unindex (Int, Int) where
  unindex ((!y0, !x0), (!y1, !x1)) !yx =
    let !w = x1 - x0
     in yx `quotRem` w

instance Unindex (Int, Int, Int) where
  unindex ((!z0, !y0, !x0), (!z1, !y1, !x1)) !zyx =
    let !d = z1 - z0
        !h = y1 - y0
        !w = x1 - x0
        (!z, !yx) = zyx `quotRem` (h * w)
        (!y, !x) = yx `quotRem` w
     in (z, y, x)

途中、 UnIndex から Unindex に変更しました (LSP が死んでいるため手動で):

$ rg UnIndex -l | xargs -I{} sed -i 's;UnIndex;Unindex;g' '{}'

IxVector

Vector / MVector に添字範囲 (i, i) を付けるだけ。案外これで十分そうです:

IxVector.hs
-- | N-dimensional @Vector@ or @MVector@ with `Data.Ix`.
data IxVector i v = IxVector {boundsIV :: !(i, i), vecIV :: !v}

(.!) :: (Ix i, VG.Vector v a) => IxVector i (v a) -> i -> a
(.!) IxVector {..} i = vecIV VG.! (index boundsIV i)

-- | Reads a value from `IxVector`.
{-# INLINE readIV #-}
readIV :: (Ix i, PrimMonad m, VGM.MVector v a) => IxVector i (v (PrimState m) a) -> i -> m a
readIV IxVector {..} i = VGM.read vecIV (index boundsIV i)

-- | Writes a value to `IxVector`.
{-# INLINE writeIV #-}
writeIV :: (Ix i, PrimMonad m, VGM.MVector v a) => IxVector i (v (PrimState m) a) -> i -> a -> m ()
writeIV IxVector {..} i a = VGM.write vecIV (index boundsIV i) a

{-# INLINE modifyIV #-}
modifyIV :: (Ix i, PrimMonad m, VGM.MVector v a) => IxVector i (v (PrimState m) a) -> (a -> a) -> i -> m ()
modifyIV IxVector {..} !alter i = VGM.modify vecIV alter (index boundsIV i)

{-# INLINE swapIV #-}
swapIV :: (Ix i, PrimMonad m, VGM.MVector v a) => IxVector i (v (PrimState m) a) -> i -> i -> m ()
swapIV IxVector {..} !i1 !i2 = VGM.swap vecIV (index boundsIV i1) (index boundsIV i2)

Destructuring の構文 DataType {..}{-# LANGUAGE RecordWildCards #-} による

動作未確認

toyboot4etoyboot4e

MArray vs IxVector vs View2d

ABC 308 D を 3 通り試してみましたが、全くスピードが変わりませんでした。 Vector にしたからと言って、無条件で速くなる訳でもないですね。以前の確認では、配列内の全要素を操作するストリーム処理が混じったために高速化できたのでしょう。

操作が限定される限り、 N 次元配列の実装は array で良いという結論になりそうです。ストリーム処理とかスライスが交じると IxVector 一択になるのかなと思います。まあ ixmap はできるけれど、それぐらいが array のメリットかなぁ……。

ひとまず View2d は削除します。

toyboot4etoyboot4e

TODO: ガバベンチ

本をあたる

地道に全部調べるのが大事なんですね! (あまりやっていない……)

ツールをあたる

ベンチマークを行う

  • String vs ByteString
  • N 回 print vs unlines
  • N 回 print vs ByteStringBuilder
toyboot4etoyboot4e

不採用: 無名関数経由で引数の順番を入れ替える

人の提出でこの手のコードを見ました。

foldM の場合

foldM の関数部分を複数行に渡って書きたい場合に便利です:

(\f -> foldM f s0 xs) $ \..

ただ $ の左辺がゴチャつきます。 foldM に関しては、特に foldForM s xs f = foldM f s xs を用意したのでそれを使っています。

fix の場合

fix 関数による再帰関数の定義・実行においても同様に、引数の順番を入れ替えて再帰関数を最後にできます:

(\f -> fix f (0 :: Int) undef start []) $ \loop !depth !parent !v1 !stack -> do

とはいえ flip fix (x1, x2, x3, x3) $ \loop (!x1, !x2, !x3, !x4) -> do .. と書いても大きくパフォーマンスに影響ありません。可読性を優先し、 flip fix を使っていこうかと思います。

toyboot4etoyboot4e

Random

Modulus だと思っていたものが modulo でした……

10 \equiv 1 (\mathrm{mod} 3) において 3modulo らしいです。また 1 つ、悲しいコミットを積み重ねてしまいました。

ixmap の正体

ixmap は添字の写像を定義する?!:

参考ツイート

ソース を見た限りは新しい配列が作られています:

ixmap :: (IArray a e, Ix i, Ix j) => (i,i) -> (i -> j) -> a j e -> a i e
ixmap (l,u) f arr =
    array (l,u) [(i, arr ! f i) | i <- range (l,u)]

しかし添字変換というのは、やりたいことの本質を突いていますね。 ixmap がその実現方法になるというのも凄い視点。。我田引水はいけませんが、人の盆栽は盗んでいこうと思います。

次に添字変換が必要になった時は、生の array を触る替わりに (Int, Int) -> Char を引数に取ることを考えてみます。実引数は (arr ! ) . f となり、 f によって添字をループさせたり回転させたり……

いや探索済み頂点の管理を考えると、新たに配列を作ったほうが安全そうです。

Prelude に古い部分がある (What I Wish I Knew When Learning Haskell)

日本語版を読んでいたら Data.List よりも FoldableTraversable を使いなさいとあって愕然としました。僕の Haskell は、始めたときから obsolete だったかもしれません。確かにリストと vector で別名の関数を使わなくてもいいよね……。

自作関数を foldForfoldForVG のようにバージョン分けしていたのも全く無意味……というわけではなく、 GHC 8.8.3 相当の vector には Foldable の実装がありませんでした。 Prelude の植え替えは、言語アップデート後に検討します。

f . runST $ do .. はなぜコンパイルできないのか

こうした事例 (playground) では謎のエラーが出ます。むしろ f $ runST $ do .. の方に特殊な推論が働いているようです。へー……。こういう知識を経験則として覚えてしまうため、言語オタクは名乗れません。プログラミング言語は難しいよ……!

https://stackoverflow.com/questions/9468963/runst-and-function-composition

ModInt に対する Enum 実装

Enum を実装すると succ, pred が使えるようになります。 (+ modInt 1) より succ と書きたいわけで便利です:

cojna/iota より
instance Enum IntMod where
  toEnum = intMod
  fromEnum = coerce

なんで preceding value が pred なのかと思いきや、 predecessor だったようです。 succ も succeding value じゃなくて successor だったのか……。英語音痴だったかもしれない。

キューが無い件

リストを引きずり回して cons していますが、書きにくい上に FILO になります。自然なコードになるようリスト関係のモナドを使う/作るとなると、いっそ ST モナドで可変 queue を使っても良い気がします。 mutable-containerscojna/iotaBuffer の利用を真面目に検討して良さそうです。

  • State モナドっぽい形 (Writer?) の利点
    引数からキューが消えます。 Haskell っぽいエレガントなコードになるかもしれません。弱点は、おそらくモナドの合成が必要になることです。 StateT を使いこなせる気がしない……

  • ST モナドを使う利点
    push 操作の引数にキューを指定できて明示的なコードになります。無難ですね〜……。

naoya_itonaoya_ito

あー、ixmap 中で配列作ってるんですね。作ってないと思い込んでいましたw

toyboot4etoyboot4e

n 次元 vector への考え

区切りが付きました。

グラフの頂点はフラットな 1 次元 Int で十分

  • グラフの生成時に n 次元添字 i を 1 次元整数の頂点 Vertex に変換しておく
  • DFS などの最中は常に Vertex にしか関心が無い
  • ただし逃げ道として Int -> i という逆変換 (Unindex) も作っておくと土壇場で融通が効く

DP における n 次元配列はやはり必要

  • これは n 次元添字越しのアクセスが頻発するため有効!
  • STUArray を使っても IxVector (以前の投稿) を使ってもパフォーマンス上は大差無い

n 次元配列として使うべきデータ型とは (array vs IxVector)

  • IxVector の方がイテレーションやスライスには強い
  • IxVector にはタプルや ModInt を始めとしたユーザー定義型が乗る
  • array を廃止した独自路線 (IxVector) で突き進んで良さそう
toyboot4etoyboot4e

TODO: intsN

concat <$> replicateM n ints するよりも 1 回の unfoldr で済ませたい

  • N 行読み取り (案外難しそう)
  • 行ベースのパースを止めて、 Int ストリームから HW 個読み取る?
    • 1 本の vector に入れてスライスするとか
toyboot4etoyboot4e

8 問体制の水 diff を埋めました

けっこう解きました。要求知識は意外と緑 diff の問題から変化ありませんでした。盆栽を止めて演習に専念したためか、レーティングも順調に上がっています。

しかしここから水パフォ後半を 4 連発しないといけないのはキツいです。 RPG においても、硬い敵が強いって相場が決まってますよ、、!

これでレーティングが上がらなければ、何をやったらいいのか分かりません。結果が出て欲しいな〜と思いつつ、 PAST 上級とか典型 90 問を埋めていこうと思います。

toyboot4etoyboot4e

あと 3 連発

全体で見ると、アクティブユーザ中の 9104 位だったりします。自分より上のプレイヤーが普段の ABC 参加者よりも多い。皆さん強いな……

naoya_itonaoya_ito

いつも思いますが Difficulty Pies の茶や緑の解答数が、他の入緑した方や入水した方に比較してもかなり少ないですよね。その AC 数でここまでのパフォーマンスが出るのがすごいなと思ってます。

精進の仕方のアプローチが一般的なそれ (一定量の問題を解く) と違いそう & それよりもずっと効率が良さそうですね...!

toyboot4etoyboot4e

いつもありがとうございます、、!

茶・緑 diff の問題を解いていないように見えますが、実は常設コンの問題 (PAST 過去問など) が反映されていないだけだったりします。

解き方としては (茶 → 緑 → 水 → 水) のような diff のローリングを繰り返しています。茶 diff の約数列挙が解けなかったりするので一長一短ですが、緑 diff が強くてニューゲーム状態になることもしばしばあります。実質的に高山トレーニングかもしれません。

toyboot4etoyboot4e

今がレーティングの頭打ちってことは無いよね

人のレーティングを除くと、緑後半に留まるケースが多くて不安になります。これから 1 年停滞するのではないかと……

よしっ、適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff (素振り)

toyboot4etoyboot4e

あと 2 回

またナップサック DP が出たり、レーティングの前提が崩壊したりで複雑な心境ですが、ついに色変が射程圏内に入りました。 PAST 上級も併せて狙っていこうと思います。

ARC で爆死しませんように、 ARC で爆死しませんように……

toyboot4etoyboot4e

典型 90 問 ★ 4

埋めました。基本的な考察・実装を網羅した良問だらけで良かったです。僕の場合は、 01-BFS や整数問題が苦手であることがよく分かりました。

01-BFS と整数問題

次々に AC できるのは最後の収穫の段階なので、それ以前のポテンシャルを高めるフェーズ、盆栽フェーズが重要だと感じました。 (外から観察する分には、解いた問題数から育てたポテンシャルも測れると思って良さそうです) 。

toyboot4etoyboot4e

典型 90 問 ★ 5

埋めました。青 diff 級の問題も『水 diff です』という体で登場します。復習しないとですね。

TODO: 復習用問題リスト (ネタバレ)
toyboot4etoyboot4e

これからどうしよう

PAST 上級をやるか……!

  • vectorconcatMap があった、、!
    ささやかなゲームチェンジャーです。リストでできることは、大体 vector でもできるということですね。

  • グリッド表示用 newtype GridView とかどうだろう
    たま〜〜に役に立つはず! というか gridShow でいいか……

toyboot4etoyboot4e

DP 第一の類型: constructN

B - Frog 2 などは、畳込み (ナップサック問題など) よりも簡単な DP とされています。改めて解くと、 MVector ベースの解答となり苦戦しました。

このような『配列を左から埋める』 (もしくは配る) 類の DP は、 constructN を使うと簡潔に記述できます。 constructN の概要は、『配列の確定した部分を参照して新しい要素を作る』というもので、かつて諦めた遅延評価にも通ずる所があります。

mod_poppo 氏の解答

constructN の使い方

適当な具体例を載っけておきます:

*Main> :{
*Main| -- 長さ 6 の配列を生成する
*Main| VU.constructN 6 $ \vec ->
*Main|   if VG.null vec
*Main|     -- 最初の要素は 1 とする
*Main|     then (1 :: Int)
*Main|     -- 次の要素 = 以前の要素の和 とする
*Main|     else VG.sum vec
*Main| :}
[1,1,2,4,8,16]

フィボナッチ数列とかも作れそうですね。オーバーキルか。。

constructN の実装

内部実装 は完全に MVector です。配列中の値が確定した範囲を都度 unsafeFreeze して高階関数 f に渡しているだけ。それでも遅延評価無しで純粋関数によって解答を記述できるのはイカしています。

constructN !n f = runST (
  do
    v  <- M.new n
    v' <- unsafeFreeze v
    fill v' 0
  )
  where
    fill :: forall s. v a -> Int -> ST s (v a)
    fill !v i | i < n = let x = f (unsafeTake i v)
                        in elemseq v x $ do
                          v'  <- unsafeThaw v
                          M.unsafeWrite v' i x
                          v'' <- unsafeFreeze v'
                          fill v'' (i+1)
    fill v _ = return v

表面上 immutable だと使い勝手が良い……もとい、配列本来の使い勝手が得られます。マッチポンプなのか悩ましい。。ただ生の配列を触ると未確定の値を読んでしまう危険もあるので、 constructN へ導く Haskell は礼賛されて良いと思います。

Haskell 由来のフレームワークが、鋭い洞察と盤石な解答を生み出す。やはり Haskell が俺たちを強くする……! 微積で高校物理を解くのと似た類の栄養を摂取できます。リロードタイムに息吹を感じる類の人は Haskell を使えば間違い無いですね。

mod_poppo 氏の提出に関して

EDPC の mod_poppo 氏は神懸っています。真っ直ぐな Haskell で最速の解答を連発、件の B 問題の提出 (48 ms) すらまったく抜ける気配がありません。常時このレベルだったのではないかと妄想し、ゾクゾクしています。シンプルに強い。こうなりたいものです。

toyboot4etoyboot4e

constructN でボトムアップに構築できるのは良いですね! 速くて楽で IntMod も載っています。

僕は iterateN n 回配る DP を思い浮かべました (解いていません) 。 constructN の発想に慣れたいです。

toyboot4etoyboot4e

正直、そこまで影響するとは思っていませんでした……! この変更を自分のライブラリに取り込むのは気が重かったですが、 $! 演算子 で代替できて安心しました (31ms) 。

updateWith :: (MArray a e m, Ix i) => (e -> e -> e) -> a i e -> i -> e -> m ()
updateWith f arr ix x = do
  v <- readArray arr ix
-  writeArray arr ix $ f v x
+  writeArray arr ix $! f v x
{-# INLINE updateWith #-}

Haskell むずかしい〜〜 (>_<)

naoya_itonaoya_ito

ああー、 $!
使い所がよくわかってなかったですが、こういう時に使うんですね...

toyboot4etoyboot4e

vector をリストと同じように扱う

vector 関連の手札を増やします。 vector を使うだけでリストの 4 倍速い提出になることもありますから、行く行くは vector へ完全移行したいと思います。

concatMap

あります。

uncons

リストへのパタンマッチで入力処理のループを書くことが多いです:

f [] = 0
f (x:xs) = x + f xs

パタンマッチの替わりに uncons を使用できます:

f xs = case uncons xs of
  Nothing -> 0
  Just (!x, !xs') -> x + f xs'

vector 版の uncons を作ると、同じように入力処理ができます:

{-# INLINE unconsVG #-}
unconsVG :: VG.Vector v a => v a -> Maybe (a, v a)
unconsVG v =
  if VG.null v
    then Nothing
    else Just (VG.unsafeHead v, VG.unsafeTail v)
f xs = case uncons xs of
  Nothing -> 0
  Just (!x, !xs') -> x + f xs'

というか vector 13 にはあるな……

groupBy

vector-13.0groupBy があります。現在の AtCoder 環境にはありません。

groupByVG, groupVG
-- | TODO: Remove on 2023 langauge update.
-- @since 0.13.0.1
{-# INLINE groupByVG #-}
groupByVG :: (VG.Vector v a) => (a -> a -> Bool) -> v a -> [v a]
groupByVG _ !v | VG.null v = []
groupByVG !f !v =
  let !h = VG.unsafeHead v
      !tl = VG.unsafeTail v
   in case VG.findIndex (not . f h) tl of
        Nothing -> [v]
        Just !n -> VG.unsafeTake (n + 1) v : groupByVG f (VG.unsafeDrop (n + 1) v)

-- | TODO: Remove on 2023 langauge update.
-- /O(n)/ Split a vector into a list of slices.
-- @since 0.13.0.1
{-# INLINE groupVG #-}
groupVG :: (VG.Vector v a, Eq a) => v a -> [v a]
groupVG = groupByVG (==)
*Main> groupVG $ VU.fromList [0, 0, 1, 1, 2, 3 :: Int]
[[0,0],[1,1],[2],[3]]

nubSort

sortUniq がありました。

スタック

スタックにはリストを使います。

toyboot4etoyboot4e

正確評価しなければサンクが溜まるやつ

『スペースリーク』で調べると色々ヒットしますね。

https://qiita.com/ruicc/items/bfa659c2ef9e1f75f7e1

サンクが増えるのを防ぐため、蓄積値を引きずり回すというのも理解できるようになりました (逆に a + b + .. + recursiveFunctionCall の形だとサンクが貯まる) 。

でも複雑過ぎて認識できません……

TODO: 末尾最適化

まだ末尾最適化が分かりません。セグ木の非再帰実装とかは、 Haskell においても高速化に繋がるのでしょうか。

toyboot4etoyboot4e

確かに、探索する深さの平均が変わって来ますね!

近々正格版セグ木を書き直して較べてみます。

toyboot4etoyboot4e

prescsan / postscan 系関数を理解した

T - Permutation | EDPC の図解を作ってみました。別解として、 prescan / postscan 系関数を使う場合を考えました。


計算方法

複数回画像を差し替えました

prescan/postscan には慣れません……。こんな感じで 26 枚描いて Zenn に投稿したいと思います。

toyboot4etoyboot4e

プレゼンがすべて

E8 さんレベルでは無いにせよ、映え画像で埋め尽くしたいですね〜

toyboot4etoyboot4e

試行回数の期待値 DP: メンタルモデルと計算式

期待値 DP の等式を理解したくて考えています。

ボツ画像

まだまだ整理中です。 i \rightarrow j は 1 回の遷移ですが、 j \rightarrow T は状態 j から T まですべての経路の集約となっており、曖昧さが残ります。うーん

ボツ画像

\sum_{j, \Rightarrow}i から j を経由したすべての経路を列挙できることにしました。さらに \Rightarrowj を始点にすることを明示すべきです。

たぶんこんな感じ。。定義に戻れば大体何とかなりますね。

J - Sushi も完全攻略できる投稿を目指していきます。

toyboot4etoyboot4e

Convex Hull Trick

TODO: 起きたらこれを最後まで読む

https://hcpc-hokudai.github.io/archive/algorithm_convex_hull_trick_001.pdf

良過ぎる、、 Z - Frog 2 の解説も入っていて神過ぎます。。。ブログも凄いしライブラリも凄い。サークルの スライド一覧 もいいですね。

https://rsk0315.github.io/library-rs/nekolib/

僕の盆栽は凡才だったか……。見上げ過ぎると首が折れそうなので、まずは CHT のスライドを実装してみます。

toyboot4etoyboot4e

constructN は単なる N 回の push_back だった

A - Frog 1 を Rust で提出してみました 。イテレータに .rev() があるのはズルいですね〜〜 (Haskell で迂闊に reverse すると、コピーが発生して O(N^2) になる気が)

toyboot4etoyboot4e

Haskell vs Rust

けっこういい勝負です。

まさか Haskell に匹敵するとは……!! 恐るべし Rust ()

Haskell 以上に厳格なコードが書けることもしばしばあるようです (たとえば VecOption が載るため番兵が要りません) 。後発の良さが見えます。

toyboot4etoyboot4e

ツイート

ABC 312 まで

  • Haskellで解くAtCoder - The curse of λ
    真面目に書かれているだけにオチが面白い

  • vector 13 の嬉しさとして認識できているもの

    • derivingVia で Unbox 実装
    • iscanl' など添字付きイテレーションの追加
    • Traversable, Foldable の実装 (mapAccumL が使える他、汎用的な関数が書けたり?)
    • uncons, groupBy の追加
  • EDPC - O | 集合 DP も大体 constructN で解ける 、確かに!
    セマンティック? 的には完全に constructN ですが、普通は手続き的に書いたほうが速いようです。リンク先では constructN 中の計算をストリーム処理に仕上げて爆速にしています。 Rust で書くとさらに速くなります。 Python を C++ で書き換えて高速化するなら、 Haskell を Rust で書き換えて高速化するのもありでしょう。

  • ポイントフリー Haskell メモ — Avendia
    2 変数関数やタプルが出てきてもポイントフリースタイルを諦めない! *** でタプルの各要素に関数適用できることが分かったため、 VU.filter (uncurry (&&) . ((testBit s) *** id)) を書くことができました。目指せ point-less style..

  • filter ((&&) <$> inRange (bounds grid) <*> ((== '.') . (grid !))) とかは使用頻度が高く便利です。

  • maxBound を番兵にするとオーバーフローする場合がある件
    少し小さめにするか、オーバーフローせず maxBound を返す wrapAdd 的な関数を使うか

  • Vimium で解答へのリンクが AC だった
    Vimium もまた AC への意志を持つ

  • AC までの時間には、考察の正確さ・バグのない実装の腕前・デバッグの速さも織り込まれる
    速さで順位とレーティングが変わるシステムは悪くなさそう。

ABC 313 まで

  • 知能の限界が近いかも知れない
    レーティングが上がりません……!!

  • 解説 AC し過ぎ
    何もしないよりはマシか、、、

  • 考察と解答を別の日に分けるやつ
    これを再開してみるか……!

  • 実力のみが足りない
    前回・前々回でレーティングが下がりましたが、出題的には解けても良い問題でした。流れは来ています。

  • (w, h) -> ((0, 0), (h - 1, w - 1)) のような関数の名前を考えたい
    むしろ Index0 という型クラスを作ってみるか

  • 水 diff 99 問
    次回青パフォを出せなければ検証は失敗に

  • 負けたよ……

ABC 314

  • 実はあるらしい。よっしゃ!、!!
  • ネタ探し中
toyboot4etoyboot4e

constructN が思っていたよりも強い気がする

constructN の正体は N 回の push_back なわけですが、 DP 配列がトポロジカルソートされている場合はこの関数で構築できます。また 1 次元の添字を n 次元にバラす関数 (unindex) を使えば、 n 次元配列も構築できる気がします。

たとえば 1 次元配列の Bit DP を書けるのはもちろん、 Sushi とか LCS (最長共通部分列) のような複雑な遷移が要求される問題も constructN で解けてしまうと思います。

イメージ
VU.constructN (h * w) $ \vec ->
  let i = VU.length vec
      (!y, !x) = unindex ((0, 0), (h - 1, w - 1)) i
      vec' = IxVector ((0, 0), (h - 1, w - 1)) vec -- Index 越しにアクセスする Vector
   in -- ..

constructIx みたいな関数にまとめる……ほどでもないかな。

ナップサック問題も 2 次元テーブルを constructN することで、 1 点毎の遷移を記述した解答が作れます。ただし畳み込みにより行毎に遷移を考えた方が筋が良いとは思います。

toyboot4etoyboot4e

constructN で解けた問題メモ

完全に自分専用スタイルとなりつつも、一旦メモ

  • 巡回セールスマン問題 (の亜種): dp[s][v]
    main 上に可変配列や手続的ループが無いのは気が楽。パターンマッチでネストが深くなってしまうものの、 when などに比べればサクッと書けそうなレベルではある。

constructN で解けそうな問題

  • J - SushiconstructNで解けると思う
  • 区間 DP もデータの持ち方を dp[len][offset] の形にすれば constructN で解けそうだし、より良い解法もありそう
toyboot4etoyboot4e

ある問題を解くと別の問題も解けるようになることもある

EDPC で bit set に対する powerset 関数を手に入れました。解説 AC できなかった難問 ABC 310 D - Peaceful Teams を振り返ると、 powerset 関数を使った解答 であっさり AC できました。

Bit set を使うのはかなりズルという気もしますが、解ければ良かろうなのだ……!

メモ: リストに対する powerset 関数。ついぞこうした再帰関数を自力で書けるようにはなりませんでした。弱点をライブラリで補っており、実装力が足りていません。

toyboot4etoyboot4e

言語アップデート 2023 の準備

2023/08/06 (日) の新ジャッジテストコンテスト (Algorithm, Heuristic) 以降、ジャッジ環境が刷新される予定です。前回 (Language Test 202001) から実に 3 年振りの更新となるようです。

リンク

英語圏の人はこうした情報を拾えるのだろうか……

コンパイラバージョン

GHC 9.4.5

言語バージョン (GHC2021)

ジャッジの package.cabal には default-language: GHC2021 が記載されています。

GHC2021 においては、 BangPatterns を始めとする多数の言語拡張が初期状態で有効になります (6.1.1. Controlling extensions) 。また import の新記法 import Module qualified as M なども使えるようになるようです (What's new in GHC 2021) 。

Haskell2010 を書かない限り GHC2021 はデフォルトで有効になるが、 GHC2025 など新しい言語バージョンが現れることもあるので GHC2021 を書いておけとあります。

GHC2021 is used by GHC if neither Haskell98 nor Haskell2010 is turned on explicitly. Since later versions of GHC may use a later GHC20xx by default, users are advised to declare the language set explicitly with -XGHC2021.

インストールコマンド

スプレッドシート からコピペしておきます。特に .cabal ファイルの定義が参考になります。

stack謎の << 記法 じゃなくて import があるのかな……

コマンド
# ユーザー名が runner 固定に見えたので,このインストールはuser-localです(apt-get を除き,multi-user環境を変えない).
# 必ずユーザー runner にログインして実行してください!
# multi-user install が必要な場合はお知らせください.

# Install prerequisites
sudo apt-get update
sudo apt-get upgrade -y
sudo apt-get install -y curl
sudo apt-get install -y --no-install-recommends build-essential curl libffi-dev libffi8ubuntu1 libgmp-dev libgmp10 libncurses-dev libncurses5 libtinfo5 llvm-14


# Install Haskell
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 BOOTSTRAP_HASKELL_GHC_VERSION=9.4.5 BOOTSTRAP_HASKELL_CABAL_VERSION=3.8.1.0 BOOTSTRAP_HASKELL_INSTALL_NO_STACK=1 sh

# Set PATH
source ~/.ghcup/env

# Set dependencies and build options
mkdir -p submission/app

cd submission

cat > submission.cabal <<'PKG_CABAL_EOF'
cabal-version:      3.4
name:               submission
version:            0.1.0.0
synopsis:           A Haskell program submitted to AtCoder
-- description:
license:            NONE
author:             submitter-anonymous
maintainer:         NONE
-- copyright:
category:           Competitive
build-type:         Simple
-- extra-doc-files:    CHANGELOG.md
-- extra-source-files:

common warnings
    ghc-options: -Wall

flag atcoder
    description:    Indicates this is on the AtCoder judge server
    default:        False
    manual:         True

executable main
    import:           warnings
    main-is:          Main.hs
    -- other-modules:
    -- other-extensions:
    build-depends:
                  Cabal ^>=3.10.1.0,
                  Cabal-syntax ^>=3.10.1.0,
                  QuickCheck ^>=2.14.3,
                  adjunctions ^>=4.4.2,
                  array ==0.5.4.0,
                  attoparsec ^>=0.14.4,
                  base ==4.17.1.0,
                  bifunctors ^>=5.6.1,
                  binary ^>=0.8.9.1,
                  bitvec ^>=1.1.4.0,
                  bytestring ^>=0.11.4.0,
                  comonad ^>=5.0.8,
                  containers ^>=0.6.7,
                  contravariant ^>=1.5.5,
                  deepseq ==1.4.8.0,
                  directory >=1.3.7.1 && <1.3.8.0,
                  distributive ^>=0.6.2.1,
                  exceptions ^>=0.10.7,
                  extra ^>=1.7.13,
                  fgl ^>=5.8.1.1,
                  filepath >=1.4.2.2 && <1.4.99,
                  free ^>=5.2,
                  ghc-bignum ==1.3,
                  ghc-boot-th ==9.4.5,
                  ghc-prim ==0.9.0,
                  hashable ^>=1.4.2.0,
                  heaps ^>=0.4,
                  indexed-traversable ^>=0.1.2.1,
                  indexed-traversable-instances ^>=0.1.1.2,
                  integer-gmp ^>=1.1,
                  integer-logarithms ^>=1.0.3.1,
                  kan-extensions ^>=5.2.5,
                  lens ^>=5.2.2,
                  linear-base ^>=0.3.1,
                  list-t ^>=1.0.5.6,
                  massiv ^>=1.0.4.0,
                  megaparsec ^>=9.4.1,
                  mono-traversable ^>=1.0.15.3,
                  mtl ^>=2.3.1,
                  mutable-containers ^>=0.3.4.1,
                  mwc-random ^>=0.15.0.2,
                  parallel ^>=3.2.2.0,
                  parsec ^>=3.1.16.1,
                  parser-combinators ^>=1.3.0,
                  pretty ^>=1.1.3.6,
                  primitive ^>=0.8.0.0,
                  process ^>=1.6.17.0,
                  profunctors ^>=5.6.2,
                  psqueues ^>=0.2.7.3,
                  random ^>=1.2.1.1,
                  reflection ^>=2.1.7,
                  regex-tdfa ^>=1.3.2.1,
                  safe-exceptions ^>=0.1.7.3,
                  scientific ^>=0.3.7.0,
                  semialign ^>=1.3,
                  semigroupoids ^>=6.0.0.1,
                  split ^>=0.2.3.5,
                  stm ^>=2.5.1.0,
                  strict ^>=0.5,
                  strict-lens ^>=0.4.0.3,
                  tagged ^>=0.8.7,
                  template-haskell ==2.19.0.0,
                  text ^>=2.0.2,
                  tf-random ^>=0.5,
                  these ^>=1.2,
                  these-lens ^>=1.0.1.3,
                  time ^>=1.12.2,
                  transformers ^>=0.6.1.0,
                  trifecta ^>=2.1.2,
                  unboxing-vector ^>=0.2.0.0,
                  unix ==2.7.3,
                  unordered-containers ^>=0.2.19.1,
                  utility-ht ^>=0.0.17,
                  vector ^>=0.13.0.0,
                  vector-algorithms ^>=0.9.0.1,
                  vector-stream ^>=0.1.0.0,
                  vector-th-unbox ^>=0.2.2,
                  xhtml ^>=3000.2.2.1

    hs-source-dirs:   app
    default-language: GHC2021
    if flag(atcoder)
      cpp-options: -DATCODER

PKG_CABAL_EOF

cat > cabal.project <<'CABAL_PROJECT_EOF'
packages: ./submission.cabal

constraints: bitvec +libgmp,
             clock +llvm,
             vector-algorithms +llvm
optimization: 2

package *
    compiler: ghc
    ghc-options: -fllvm -Wall

CABAL_PROJECT_EOF

echo "main = return () :: IO ()" > app/Main.hs


# Configure and build dependencies
cabal v2-update
cabal v2-configure --flags="+atcoder"
cabal v2-freeze
cabal v2-build --only-dependencies


# Clean up the things only needed for installation
rm app/Main.hs
rm -rf ~/.ghcup/bin/ghcup ~/.ghcup/cache ~/.ghcup/logs ~/.ghcup/tmp ~/.ghcup/trash ~/.cabal/logs

submission.cabal

インストールコマンドに埋め込まれた submission.cabal からの抜粋です。未確認ですが、たぶん LTS 21.6 が対応するのでしょうか:

パッケージ一覧
executable main
    import:           warnings
    main-is:          Main.hs
    -- other-modules:
    -- other-extensions:
    build-depends:
                  Cabal ^>=3.10.1.0,
                  Cabal-syntax ^>=3.10.1.0,
                  QuickCheck ^>=2.14.3,
                  adjunctions ^>=4.4.2,
                  array ==0.5.4.0,
                  attoparsec ^>=0.14.4,
                  base ==4.17.1.0,
                  bifunctors ^>=5.6.1,
                  binary ^>=0.8.9.1,
                  bitvec ^>=1.1.4.0,
                  bytestring ^>=0.11.4.0,
                  comonad ^>=5.0.8,
                  containers ^>=0.6.7,
                  contravariant ^>=1.5.5,
                  deepseq ==1.4.8.0,
                  directory >=1.3.7.1 && <1.3.8.0,
                  distributive ^>=0.6.2.1,
                  exceptions ^>=0.10.7,
                  extra ^>=1.7.13,
                  fgl ^>=5.8.1.1,
                  filepath >=1.4.2.2 && <1.4.99,
                  free ^>=5.2,
                  ghc-bignum ==1.3,
                  ghc-boot-th ==9.4.5,
                  ghc-prim ==0.9.0,
                  hashable ^>=1.4.2.0,
                  heaps ^>=0.4,
                  indexed-traversable ^>=0.1.2.1,
                  indexed-traversable-instances ^>=0.1.1.2,
                  integer-gmp ^>=1.1,
                  integer-logarithms ^>=1.0.3.1,
                  kan-extensions ^>=5.2.5,
                  lens ^>=5.2.2,
                  linear-base ^>=0.3.1,
                  list-t ^>=1.0.5.6,
                  massiv ^>=1.0.4.0,
                  megaparsec ^>=9.4.1,
                  mono-traversable ^>=1.0.15.3,
                  mtl ^>=2.3.1,
                  mutable-containers ^>=0.3.4.1,
                  mwc-random ^>=0.15.0.2,
                  parallel ^>=3.2.2.0,
                  parsec ^>=3.1.16.1,
                  parser-combinators ^>=1.3.0,
                  pretty ^>=1.1.3.6,
                  primitive ^>=0.8.0.0,
                  process ^>=1.6.17.0,
                  profunctors ^>=5.6.2,
                  psqueues ^>=0.2.7.3,
                  random ^>=1.2.1.1,
                  reflection ^>=2.1.7,
                  regex-tdfa ^>=1.3.2.1,
                  safe-exceptions ^>=0.1.7.3,
                  scientific ^>=0.3.7.0,
                  semialign ^>=1.3,
                  semigroupoids ^>=6.0.0.1,
                  split ^>=0.2.3.5,
                  stm ^>=2.5.1.0,
                  strict ^>=0.5,
                  strict-lens ^>=0.4.0.3,
                  tagged ^>=0.8.7,
                  template-haskell ==2.19.0.0,
                  text ^>=2.0.2,
                  tf-random ^>=0.5,
                  these ^>=1.2,
                  these-lens ^>=1.0.1.3,
                  time ^>=1.12.2,
                  transformers ^>=0.6.1.0,
                  trifecta ^>=2.1.2,
                  unboxing-vector ^>=0.2.0.0,
                  unix ==2.7.3,
                  unordered-containers ^>=0.2.19.1,
                  utility-ht ^>=0.0.17,
                  vector ^>=0.13.0.0,
                  vector-algorithms ^>=0.9.0.1,
                  vector-stream ^>=0.1.0.0,
                  vector-th-unbox ^>=0.2.2,
                  xhtml ^>=3000.2.2.1

なお今回からジャッジ環境で ATCODER シンボルが定義されるようになったため、 #ifdef DEBUG の替わりに #ifndef ATCODER が使えます:

    default-language: GHC2021
    if flag(atcoder)
      cpp-options: -DATCODER

コンパイルコマンド

$ cd submission
$ source ~/.ghcup/env
$ cabal v2-build --offline && cp $(cabal list-bin main) ../"

以上を参考にローカル環境をアップデートしていきます。

toyboot4etoyboot4e

しまった、ビルドが終わらない

テストコンテストに参加したいんです頼む〜〜〜〜

toyboot4etoyboot4e

しまった、ビルドが終わらない (2)

新環境で提出のビルドが長いのは完全に盲点でした。 cabal v2-build が遅いのか、リンクが長いのか。

追記: 『ジャッジ初回実行時のウォームアップのために1~2分ほどステータスがWJのままになることがあります。』これが原因だと良いですが、流石に長い気もして不安です。

toyboot4etoyboot4e

言語アップデート 2023 に対応完了

意外と変更がありません。デフォルトの言語拡張が増えて嬉しい程度です。

https://github.com/toyboot4e/toy-lib/pull/1

次回の ABC 以降は、過去の問題も 2023 仕様で提出できるようになる風です。

toyboot4etoyboot4e

IsoUnbox

cojna/iota の更新で IsoUnbox を知りました。

https://github.com/cojna/iota/pull/187

As を使うと、 vector 保存時に内部データ表現に変換できるようです。これなら sum type もタグの部分を Int にするなどして保存できます (また ニッチ最適化 で省メモリできるかも) 。

https://hackage.haskell.org/package/vector-0.13.0.0/docs/Data-Vector-Unboxed.html#t:As

vector-th-unboxと TemplateHaskell に別れを告げ、言語サーバーはクラッシュしなくなるのか……!?

toyboot4etoyboot4e

HLS が動いた!

遂にクソデカテンプレートに対して HLS が動くようになりました! 大規模なリネームが簡単になったのはありがたいことですし、 変数の型を表示できるようになった のがデバッグで有利です。

ただ『関数型言語 Rust』の難しさが Haskell と似ていたことを考えると、そもそも関数型スタイルだと型が複雑になり、 LS の補助があってもわけが分からない可能性は高いです。

Unbox の実装方法

Template Haskell を消せるようになったおかげで HLS が動いたと思います。 vector 13 において、 Unbox の実装方法は 3 種類ありました:

  1. 単なる primitive の newtype である場合
    UnboxViaPrime を使います。が、 2. との違いが分かっていません。今日の cojna/iota で使われていないのを見ると、使わないほうが良いかもしれません。

  2. newtype である場合
    Unboxed の冒頭の通り『 GeneralizedNewtypeDeriving』ができるようです。この拡張は GHC2021 で自動的に有効になります。

  3. newtype ではない場合
    As を使うと vector-th-unbox と似た (やや冗長な) コードで実装できます。

HLS のバージョン

今の所最新版を使って良いようです。 nixpkgs 上にあるのは 2.0.0.0 ですが、もう次が出ているようです。

GHC version support — haskell-language-server 2.1.0.0 documentation

一応動く `shell.nix` (unstable)
{ pkgs ? import <nixpkgs> {} }:

let
  stable = import <stable> {};
in

with pkgs; mkShell rec {
  nativeBuildInputs = [
    pkg-config
  ];

  packages = [
    stable.online-judge-tools

    haskell.compiler.ghc945
    (haskell-language-server.override { supportedGhcVersions = [ "945" ]; })
    haskellPackages.ghcid
    haskellPackages.ghcide

    stack
    cabal-install
  ];

  buildInputs = [ ];

  LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
}

Nix 分かりませんだ……

toyboot4etoyboot4e

atcoder-cli

しばらく前から最新の online-judge-tools と組み合わせると動かなくなっています (lxml が無いとかなんとか。おま環?) 。 online-judge-tools 単品だと動くので、もしかして oj 単体に乗り換えるべき……?

naoya_itonaoya_ito

いずれ昨今更新がないので、私も最新だと思いますが動いています

Python の lxml のインストール周りは何かとよくはまりがちなので一度 python 環境を洗ってみると良いかなと思います。

naoya_itonaoya_ito

atcoder-cli が参照している oj と、直接コマンド打った時の oj が違うもの見ててそれぞれの oj が別の python 環境に紐づいている、とかでしょうか...?

toyboot4etoyboot4e

ありがとうございます!

atcoder-cli が参照している oj と、直接コマンド打った時の oj が違うもの見ててそれぞれの oj が別の python 環境に紐づいている、とかでしょうか...?

これをヒントに解決できました!!

toyboot4etoyboot4e

oj / acc アップデートメモ

問題

NixOS において online-judge-tools をバージョンアップした後に npx acc new abc150 を実行するとエラーが発生する。

解決方法

~/.config/atcoder-cli-nodejs/config.json 中の oj-path キーを削除する。

原因

online-judge-tools のバージョンアップ時に oj のインストールパスが変化するが、 npx acc は古い oj を参照していた。

確認方法

online-judge-tools を stable/unstable チャンネルのバージョンに切り替えた際に、 atcoder-cli から見た実行ファイルパスが変わらない:

$ npx acc check-oj # stable
online-judge-tools is available. found at:
/nix/store/xihjnwav23s0mqd4kh01mwcw22dyfxa4-python3.10-online-judge-tools-11.5.1/bin/oj
$ npx acc check-oj # unstable
online-judge-tools is available. found at:
/nix/store/xihjnwav23s0mqd4kh01mwcw22dyfxa4-python3.10-online-judge-tools-11.5.1/bin/oj
toyboot4etoyboot4e

検証: 水 diff を 100 問解けば水色コーダーになれる説

入水ならず! 青 diff が解けかかったかけれども及ばずでした!

このスクラップは2023/08/13にクローズされました