典型盆栽 (Haskell 盆栽 3) → 入水ならず
このスクラップは
本やガジェットをひけらかしつつ、水色コーダーを目指す投稿です。
方針
水 diff を 100 問解けば水色コーダーになれる説 (願望) を検証します。スクラップ作成時の進捗は 32/100 です。
注意
Haskell 目当てなら、もう見所が無いと思います……!
良かったもの
ガジェット
-
Kindle Scribe
オライリー本を超えた大画面で技術書を読みます。 -
AFB7218
適度な負荷で読書週間を作ります。
本 (Haskell)
-
すごいHaskellたのしく学ぼう!
入門書と言いつつ十分難しい。モナモナした章は消化不良です。 -
【電子版単体】Haskellで戦う競技プログラミング 第2版
すぐ役に立つ + 長く役に立つ! 本当に良い本です。 -
Haskell High Performance Programming
娯楽用に。
本 (競プロ)
-
競技プログラミングの鉄則
これと典型 90 問 (〜 ☆ 3) のおかげで茶色になれました。 -
問題解決力を鍛える!アルゴリズムとデータ構造
これと TDPC のおかげで緑色になれました。 -
アルゴリズム実技検定 公式テキスト[上級]~[エキスパート]編
これで水色になる予定です。
Podcast
-
The Haskell Cast #13
Nix で環境構築し、 Coq で証明し、 Haskell で実装する。ナードとはこうでなくては……! -
Misleading chat #88, #108
貴重な Haskell トーク、 Verse トークです。 -
Rebuild #352, Rebuild #357
Web 記事 も必見!
Youtube
-
AtCoder Live
解説記事が分からないときは解説放送を見ます。 -
Festival Code
コモンセンス強化のため。 -
こたつがめ
ABC 無双など。転生者ですか……?
消化用ノート
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 (未知の範囲)
- 典型収集漏れ
- 過去問がすべて解ける考察フローチャート作成
典型ノート
水 diff 演習が落ち着いてから、取りこぼした典型問題を拾って行きたいと思います。
少なくともこれだけ零しているのか……怖すぎる!
回収済みの解法
- Brute-force 全探索
- 2 分探索の判定問題で最大化・最小化
前提条件となるテンプレート
- 素数列挙、素因数分解、 ModInt
- セグメント木、 Union-Find
- 2 分探索
お手軽テンプレート入り
- 座標圧縮 (1 次元)
- 尺取り法
- 転倒数 、辞書順
-
作用素付きモノイド
- ダブリング
- 全方位木 DP
- Rolling Hash
- topSort, SCC
- dfs, bfs
- dijkstra, 逆グラフ上の dijkstra
得るべきテンプレート
- 2 つの配列をまたぐ尺取り法のテンプレート?
- Rollback 付き Union-Find
- 遅延セグ木
解けるべき典型
- 平均値・中央値の最大化
- 半分全列挙
-
01-BFS
- 枝刈り BFS
-
行列累乗
- ABC 293 E
-
2 次元グリッドの座標圧縮
https://pione.hatenablog.com/entry/2021/03/03/060451 - 2-SAT
-
二部マッチング
- 重み最大化一般マッチング
-
最大流量
- 最小カット
- 最小費用流
- 燃やす埋める ?
- Floor min (格子点の数)
- 主客転倒
- 包除原理 (激ムズ……)
- 中国剰余定理 (CRT)
- 3 分探索
- 拡張ユークリッドの互助法
- 凸 性?
避けていた問題
-
ゲーム
- Nim ゲーム
- Grundy 数
DP をもう一度
- 集合 DP (Bit DP)
- 桁 DP
- 区間 DP
- 期待値 DP
高度典型
- 2 次元の Union-Find
- Mo
- FFT
- 重心分解
- HL 分解
日報 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 を求めるのが遅すぎるのかもしれない。リストのマージ処理をIntMap
のunionWith
に置き換えたところ、全く同様に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
もアロケーションと同様に の処理で TLE となった。 あ使用した添字のみO(N) 0
にリセットして通しているみたい。これなら (もしくはO(N^2) ) ではなくO(N^2 log N) で済む。同様に効率的なリセットをすると通った。まあ座標圧縮版が楽で良いか。O(N log N)
-
★ ABC 261 F: なんだかんだ近いコンテストでは類題というか似た問題が出る模様。一見区間木で解けそうだが、 ABC 259 E と同様に TLE してしまう。 WA も出て心が折れそうだ。これも解説を見てしまおう。
-
05/05: バ
-
☆ ABC 262 D: 大体全探索だと思いますが、再帰的リスト生成が苦手。何とか提出するも TLE. 解説を見て考察失敗だなと……。これで水 diff 下位とはどういうことなんだ。。
1 つずつ処理するデータ範囲を広げていく普通の DP を n 回実行! 時間制限ギリギリで AC できた。これが brute-force DP か。全探索を信じよ! -
ABC 250 F:
IntMap
をscanl'
したり区間木に入れて失敗するシリーズ。このアンチパタンめちゃめちゃ多いな。そこからどう高速化するかは多種多様だけれども。。
今回重そうなのは比較だが、普通に比較すると なんだよな。。 → 比較結果を配列に入れておくだけで通ってしまった 。本当にそれでいいのか……?! 最低限解説は読んでおこう。O(N^2)
-
☆ ABC 262 D: 大体全探索だと思いますが、再帰的リスト生成が苦手。何とか提出するも TLE. 解説を見て考察失敗だなと……。これで水 diff 下位とはどういうことなんだ。。
-
05/06: バ
-
ABC 255 E: 最初の要素を決めると以降の要素が一意に定まる。最初の要素を決めた後のスコアは
程度で計算できるため、最初の要素の候補を絞れば良さそう。カウントが最大の値を『ラッキナンバー』のいずれかに合わせるようなN logM x
をすべて調べれば良いか。 → TLE. ハッハッハッ。確かに、たとえば重複 0 だったら だ。。N^2 log M
全然解けないな。高速化したら WA まで出ちゃったよ。候補を絞り込んだのがダメだったみたいで完全に考察失敗だ。今週ずっとこんな感じだわ!
いやカウントはループを逆にして 程度で計算できるな。M log N くらいで解けるのでは。 → 解けた。やはり天才だったかwNM log N
終わってみれば brute-force 全探索。この程度の問題を反射的に解けないと水パフォは厳しいか……。 -
ABC 258 E: 箱の数が異様に多いため、規則性を見つける必要がありそうです。ループを見つけるための試行回数は最大
回。あるいはダブリングした方が簡単そうですね。ダブリング用の配列は尺取り法のテンプレートであっさり求められそうです。N
→ WA. くっ……! → AtCoder Companions を見ると 確かにループの計上を忘れていた……。再度提出するも WA2. 再び AtCoder Companions を見ると 確かに尺取り法が境界の場合を見逃していた……。実践基準で考えると全然惜しくないよ。。
ど典型だと思ったけれど解説はダブリングじゃない……? ああループ。テンプレートがあるなら、ダブリングした方が簡単かなとは思う。必ずループする (行き先がダブる) というのは鳩の巣原理と呼ばれるそうで、最近の黄 diff でもあったみたい。
-
ABC 255 E: 最初の要素を決めると以降の要素が一意に定まる。最初の要素を決めた後のスコアは
-
05/07:
-
ABC 257 E: まず桁数を最大にして、次に大きい桁から最大化していくという貪欲解で十分そう。 → WA (AC 21/30). えぇ WA か……。考察ミスで WA が多くて痛いな。
→ テストケースを作ってみたら反例が見つかって AC. 最小コストで埋めた桁より小さい値に書き換えてはいけない。
制限時間が怪しいものの完全勝利! まあ D で出たら緑 diff になった気がする。 - ABC 298 F: 以前解説を見ていたおかげで余裕で解けてしまった。枝刈り万歳!
-
ABC 292 F: 解説 AC じゃないと無理だった。
四辺形の短辺に正三角形の底辺を合わせる。角 だけ三角形を回転させる。三角形の 1 つの角を四辺形の辺に合わせる。もう片方の角が四辺形からはみ出ないように可能な限り回転させる。\theta
なぜか をacos だと思っていて苦戦しました。1 / \cos - ABC 293 E: 線形な漸化式の一般解を行列累乗で求める。行列累乗は行列の積のダブリング (繰り返し二乗法) によって求まる。この公式解説を読むと、 フレンズさんの解説 (Twitter) も同じものだったことが分かった (演算子のダブリングだった) 。
- ABC 278 F: 木探索として考察して解いた。辺を区別しない方が良い。辺を区別すると、計算量が莫大に増える場合があって良くなかった。アルファベットを頂点とし、辺を消費しながら探索していくと爆速。 DP で解くのは大変そうなので止めた……。
-
ABC 257 E: まず桁数を最大にして、次に大きい桁から最大化していくという貪欲解で十分そう。 → WA (AC 21/30). えぇ WA か……。考察ミスで WA が多くて痛いな。
第 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: バ
-
05/11: バ
-
鉄則 029: テトリス的な問題。 imos 法の累積和でもない。
の解答しか思いつかずサレンダー。O(N^2)
セグメント木だと 1 点の値更新が ), N 点の値更新はO(log N となる。しかし遅延セグメント木だと区間更新もO(N log N) になるとか。 PAST 上級本を読もう。O(log N)
-
鉄則 029: テトリス的な問題。 imos 法の累積和でもない。
-
05/12: バ
- [典型 036]: マンハッタン距離の上手な求め方 。 E8 さんの数学記事 を抑えておくべきか……!
-
[典型 037]: ☆ これ解けたかったあああああ。また時間制限が非常に厳しく、セグメント木の実装で
VUM.unsafeRead
,VUM.unsafeWrite
を使って初めて AC.
-
05/13: バ 30
- [典型 051]: ☆ 鉄則本でやったはずだがノーマークだった。どう頑張っても通せず。
- ABC 301: 4 完 (ABCD).
-
05/14: バ 25
400 AC
水 diff は 40 問解きましたが、 9 割方解説 AC でした。非常に厳しい……!
AC heat map (max difficulty)
水 diff は 1 日 1 問が限界のため演習期間がやたら長い
難度感
本番は時間制限が厳しいのが気にかかっています。水 diff までは trivial という感覚に到達しないといけません。今は茶〜緑も大分怪しいけれど……。
これまでの主な学び
全探索を信じよ!
今後の方針
もうすぐ演習量が こちらの色辺記事 に匹敵してしまいます。この先三ヶ月で現在のタスクをすべて消化し、それでも色変が厳しい場合は、 ARC 特攻するか高度典型を狙うしかないです。。
水 diff streak 完了
ABC 255 ~ ABC 300 の水 diff の問題を全問解ました。
- 手札があっても解けないことがしばしばありました。
- 典型漏れは必ず突かれます。僕の場合はゲーム DP とか……。
- 判定問題の 2 分探索が発想にありませんでした。考察フローチャートを作ってみたいです。
- 転倒数もまず思いつきません。
次は典型 90 問を回って、 ★ 5 が半分以上自力で解けたら言うこと無しなんですが……!
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
が再帰的になるのが分かりました。 - 平衡処理が無くてあれっとなりました。
- 確かに IntMap も
lookup
とかinsert
は ですね。O(\min(n ,w)) - ここで W とは木の最大の深さ (64) なので、操作コストは定数です。
- まあ平均したら
なんじゃないでしょうか。O(\log n)
- 確かに IntMap も
- 赤黒木は検索・挿入は速いがマージが遅い。
IntMap
(big-endian patricia tree) はマージもそこそこ速い。
QuickChecking patricia trees
上の論文には バグがあるらしい ので、その修正を含むという Jan Midtgaard. QuickChecking patricia trees, 2018 を読んでいきます。
- QuickCheck で patricia tree をテストして正しさを保証、しようとして上記論文のバグを発見した
- テストにはプロパティベースのテストとモデルを使用したものがある?
- WIP
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"
Haskell ネタ 2
高速化 3 本立てです。
unsafeRead
, unsafeWrite
が効いた (~200 ms)
1. Vector の 典型 90 問: 031 の TLE を取るために、セグメント木内の Vector
を unsafeRead
, 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
{-# INLINE #-}
すると遅くなった (~200 ms)
2. 再帰関数を 同じく 典型 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 ループは等しくなります:
- リスト
forM_ [1 .. pred nSets] $ \s -> do
- Unboxed vector
-- rangeVG !i !j = VG.enumFromN i (succ j - i)
VU.forM_ (rangeVG 1 (pred nSets)) \s -> do
- Stream
forMS (rangeMS 1 (pred nSets)) \s -> do
ガバベンチ
ABC 301 E の Bit DP における 3 重ループ (集合、遷移元頂点、遷移先頂点) の書き方を変えて提出しました:
- List (
forM_
+[l .. r]
): 2477 ms - Vector (
VU.forM_
+rangeVG
): 2309 ms - Stream (
forMS
+rangeMS
): 2068 ms
やはり stream が圧倒的に速かったです。テンプレートも stream を使うよう変更しました。
やはり Rust と張り合えるような Haskell の次の言語に期待したい……!
repM_
ガバベンチ 4: 単なる再帰関数でループが書けることに気付きました:
{-# 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)
- ループ (
repM_
): 2047 ms
衝撃の結果です。まさか生の vector stream を触る意味がなかった……? 随所で比較してみたいと思います。
初歩的な回転の復習
人は皆、二次方程式の解の公式を忘れ、平方完成するようになる。信じられないかもしれないが本当の話だ。遠くない内に君も経験することになる。
マンハッタン距離の典型問題で 45 度回転が出てきました。回転、回転……? 回転といえば複素数、思い出さねば……!
関数のべき級数展開
無限回微分できる関数
オイラーの公式
回転
特に
基底を元に回転行列を求めるやつ
忘れた (そもそも線形代数が正しくインプットされていない orz)
マンハッタン距離と 45 度回転
2 点間の距離を
45 度回転は、上の回転行列を
感想
正しくインプットできなかった数学は、改めて学び直そうかと思いました。 FFT も典型らしい……!
グリッド上の 01-BFS or 枝刈り BFS
問題
グリッド上の始点から終点へ駒 (例: 飛車) を動かす。始点から終点まで移動する最小の行動回数を求めよ。
方法 (01-BFS or 枝刈り BFS)
普通にグラフ探索を書いても通りますが、重複除去が難しく TLE しがちです。 01-BFS を使うと安定します。
参考になるのは 典型 043 の解答コード、 けんちょんさんの ABC 246 E 解説 など。とくにけんちょんさんの解説が良さそうです。
僕は重複した探索の除去を頑張っていましたが、そうかスコアで枝刈りすればよいのか……?
問題例
コードメモ
Python の倍くらいのコードになって非常に厳しい……
01-BFS
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
モノイド (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
: 置換配列です。- 結合的演算
<>
: 置換配列の合成を行います。 - 単位元
mepmty
: 恒等置換を表す配列[0, 1, 2, ..]
を返します。 [1]
- 結合的演算
- 作用の対象
a
: 置換される整数Int
です。 - 作用
*
:* :: 置換配列 -> Int -> Int
は置換を実行します。
作用は置換配列を置換関数Int -> Int
に変えるものであるとも捉えられます。
ダブリング (binary lifting)
作用付きモノイドはダブリング (binary lifting) の対象となります。ダブリングの際は作用付きモノイドを事前に結合できることを活かし、
ダブリング後は、
ここで
なぜ作用付きモノイドなんてものを考えるのか
考察と実装が穴埋めゲームになって簡単になるためです。
なぜ作用付きの半群ではなく作用付きモノイドを考えるのか
なぜ mempty
が必要なのかという点ですが、いや別に無くても良いかもですね……。 n == 0
の場合で分岐すれば単位元は必要ありません。
ただダブリング以外の例では、分岐が減ると嬉しい場合が多いです。 mempty
を遠慮なく畳み込み (結合 <>
) の初期値として使えます。モノイドである必要は無いかもしれませんが、モノイドとした方が便利であるため、モノイドに限定して考えています。
どうしても半群が モノイド にならない場合、 Maybe で単位元 Nothing を加えれば半群がモノイドになります。分岐を外出しできて便利なので、やはりモノイドだけを考えれば良いのだ!
モノイド を表現するには
Haskell で作用付き1. 型クラスを使う
やはり class OperatorMonoid
を作るのが最も美しい方法に思えます。 Maybe でラップすると半群がモノイドになる、モノイドのタプルはモノイドの直積であるなど強力な機能を利用できます。しかしモノイドへの慣れが必要なので、一旦置いておこうと思います。
2. データ型の中に関数を入れる
data
や newtype
を使っても良さそうです [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 だと思うのですが、正しい説明では無い気もして省略しました。 -
遅延セグメント木
実装中..
確認中……
もう少し整理できそうなので色々あたってみたいです。
-
恒等な置換を表す配列
[0, 1, 2, ..]
の生成には、長さの引数が必要です。そのため置換配列をモノイドのインスタンスにすることは (通常) できません。ただし配列の長さが型パラメータで分かっている場合はmempty
を定義できます。 ↩︎ -
型クラスを使用せずに関数 (の組) を渡すことを dictionary passing と呼ぶそうです。 ↩︎
AtCoder 以外のジャッジ環境
他サイトを覗いてみました。使用可能ライブラリの一覧すら見つからなくて辛い。ひとまず AtCoder におけるテンプレートを提出してみると……
- Yukicoder: primitive が無い、 extra が無い
要望を出せば通りそうな気はしています。 - Codeforces: vector すら使用不能、コード長に制限あり
C++ 前提という感じがしますね。 - Topcoder: 実名前提なので登録したくない
もはや登録したくない。
結局富豪スタイルで挑戦できるのは AtCoder だけでした。その AtCoder でもジャッジ環境の更新は隔年であり、競プロって凄くローカルなものだなと認識を改めました。
そもそもファイル分割ができない特殊な環境ですし、今から Common Lisp のライブラリを大量に入れようと思っても不可能ですものね。
Codeforces を覗いた後、 AtCoder の窓口は随分と広かったんだなと感激しています。 Haskell のジャッジ環境も奇跡的に良くできていて、先人に感謝するばかりです。今の AtCoder が無ければ、一生 Haskell を書く機会が無かったかもしれません。
AtCoder Charts
パフォーマンスの変化を可視化してもらいました。もっと水パフォが欲しい (。ω。)
遅延評価セグメント木を実装してみた
PAST 上級本 と cojna/iota を参考に遅延評価セグメント木を実装しました。 cojna さんのコードは入出力やグラフを始め、全体的に理想的な構成に見えます。今後も参考にしていきたいです。
029 - Long Bricks(★5)
典型 90 問:これを通しました。 とにかく大変でした 。色々な自信を失います…… (定期) 。今後リファクタリングしていきます。
現在の疑問点
-
バグは無いのか
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 モデルで共有してみたい……!
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. をダブリングすると、『頂点 v
の n
番目の親』を
LCA を求める
-
(v1, v2)
を同じ深さまで移動させて(v1', v2')
とする - 2 分探索で LCA を求める
判定問題parentN d v1' == parentN d v2'
はd
に関して単調です
. root
|
. # 長い木の LCA も O(log N) で求まる
|
~~~~~~~~~~~~~~~~
| |
| |
| |
/ \
V1 . . v2'
. v2
LCA を経由した畳み込みを行うには
本題の畳み込みですが、事前準備でもう 1 つ配列を作成します:
- 配列 3: 頂点 → 親へ移動する際に結合するモノイド
ダブリングします
畳み込みのシグネチャとしては、おそらくこうなるはずです: けっこう難しい
実装しよう…… (・ω
ABC 235 E)
実装してみた (モノイドをダブリングする時に、頂点の移動も追う必要があって苦戦しました。モナドか何かで既存の lca
にモノイドの計算を差し込めたら良いのですが。
スピード面
1/2 の確率で TLE になります。遅すぎて使えない。。 フレンズさんは 617ms で通している ので、もう少し上手く実装したいです。
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 が生成される気配が無く……?
PAST 対策中
2023/06/03 (土) 13:00 ~ 18:00 にアルゴリズム実技検定があります。上級認定を目指すなら、 15 問中 12 問解けねばなりません。もう遅いですが対策します。
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. 難問にチャレンジ! [エキスパート編]
見てません。既知の手法の組み合わせ?
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 を復習します。
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]: 行も列も
で回転できるような 2 次元グリッドの持ち方?O(1) Seq
列じゃダメだ…… - [O]: この記事 を参考に全探索して失敗?
Upsolve
- J: 解説: 地道に算数を頑張る。そうか……
-
L: やはり DP らしいが……? → 総和の mod を固定すればナップサック DP になる。なるほど。
- 試験対策としては、きっと ナップサック DP とセグ木 DP しか出ない んだから、どちらかに当てはめて試行錯誤すれば良かろうなのだ!
- ナップサック DP は初期状態からの遷移に対する番兵が重要……が、 WA ……!
- q の変域が
であることに注意して AC (PAST は WA を狙ってくるな)[1, n + 1]
-
N: 解説:
となるよう転置すれば十分間に合うh \le w - 制約
はそうやって使うのか……!H \cdot W \le 2 \cdot 10^5 - しかし TLE? 定数倍の高速化ではビクともしません。
Seq
の がデカいせいなのか 。log VM.MVector (VUM.MVector Char)
をリングバッファにするしか無いかもです。
- 制約
- O: 解説: 2 次元 FFT 。 FFT をやらねば!
感想
時間があっても 10 or 11 問が限界でした。粘って N を通せばあるいは。。
PAST の問題傾向がかなり好み
PAST は基礎練に良いですね。 PAST をやれば、 ABC の D までは全く苦戦しなくなる予感があります。それは早解きに繋がりますし、高難度問題に挑めば解ける問題の上限も伸びます。総じて筋肉質な実戦向きの問題セットです。
-
細部まで詰めて考えないと、すぐ WA になる
序盤からかなり歯応えがあって頭を使います。高山トレーニング? -
出題範囲が狭い
PAST の範囲を得意分野として固められます。 -
高難度の問題もちゃんと出る
水 diff 中盤級の考察・実装や上級アルゴリズムも出ます。
鉄則本、典型 90 とか EDPC のような問題セットは、限界を広げるには良いですが、実戦のパフォーマンスに直結しにくい面がありました。その成分は PAST 過去問で補完していきたいと思います。
受験結果
64 点で中級でした。規約上怖いので感想はナシで!
順位表、解説、人の提出も読めなくて悲しい……!
間違えても解答をコミットできないように、 *past*
を .gitignore に追記しました。 PAST でも oj/acc はそのまま使えます。
競プロには意外と『コツ』が存在する
過去の自分 (伸び悩む競プロ初心者) には、案外『じゃあまず 100 問解いて!』以外のアドバイスがあり得る気がします。たとえば今の僕が知っているコツとしては:
Haskell が教えてくれたこと
コツは派生して多くの知識や解法に繋がります。逆に『コツ』を掴めていない分野を列挙すると、狭く具体的になりがちです。コツを掴みたいですね……!
苦手〜解きづらい問題例
- 商列挙とか半分全列挙のような効率的な数え上げ
- 超頂点を用意して辺の数を減らすパターン
- ゴールから考えるタイプの動的計画法
競プロに限らず、たとえば Haskell においても『1 つの式の中にすべての文脈が載っているため理解しやすい』などの発見はあったはずですが、『コツ』言えるほど強力になることはまずありません。結局『じゃあまず 100 問解いて!』が最適なのかもしれません。
まず 100 問解くためのコツってありますか……? (血眼) 習慣にはニワトリと卵問題がッ
また 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
使用したテクニック
-
do
ブロックの中でやりたい放題
辺の入力をパースしながら、次数のカウンタ (MVector
) を書き換えています。 -
traceShow
でデバッグ出力
スニペット定義してprint
文のようにtraceShow
を使っています。 -
気分はモナディック版
until
until :: (a -> Bool) -> (a -> a) -> a -> a
のうち、ステップ(a -> a)
と初期値a
をモナディックにしたい! この場合、do
ブロックの中で初期値を作り、再帰関数でループしてしまいます。 -
fix
関数で再帰関数の定義・実行を同時に行う
ループをシュッと書く常套手段です。なぜこうなるのか知りませんが、不動点コンビネータ凄い。 -
fmap
によりIO
モナドの中身をポイントフリースタイルで操作
(\x -> f <$> g x)
は(f <$>) . g
またはfmap f . g
と書いてポイントフリーにできます。 -
foldForM
で低インデントを保つ
foldForM a b f = foldM f a b
としています。関数部分を最後の引数にしたおかげで、インデントの数を減らすことが出来ています。 -
<=<
演算子でモナディック関数合成
f >>= g
とg <=< f
は等価です。とりわけf
が長い場合は、g <=< f
と書くと端的な表現になります。
それ Haskell を使っている意味あります???
だ、だって、ポイントフリースタイルで関数合成する喜びは他の言語で味わえないから……!
それ他の Haskller が読めます?
無理かも……
逆に純粋関数型で書く意味はあるのか
ケースバイケースだと思うので、具体例を考えてみました。
-
ナップサック 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 で書く意味は無いが、他の加点を考えて総合はプラス! ポジショントークができました。
- 手続きを純粋関数型言語で書く利点
dbg 関数参考にさせていただきました 😀
ありがとうございます 🫶 🫶 🫶
私も naoya さんの accumArray
で DP を解くアイデアが大好きです。
シナプス Unbox 化してんのかい!https://www.google.com/search?client=firefox-b-d&q=肩にちっちゃいジープ乗せてんのかい!
AC 503 ボルト
記念用・観賞用です。
とか言って見返したことは無いですね……
AtCoder Problems のスクリーンショット
ABC 305 で 503 AC に到達
水色になりた過ぎる来歴
水色になりた過ぎる来歴
PAST と常設コンが半分くらい
2019 年に Rust で ABS を挫折 (面白くなかったんだろうな……)
平日に問題を解いたらレーティングが上がった
2019 年に Rust で ABS を挫折 (面白くなかったんだろうな……)
AHC 020 に参加してみた
AHC 初参加で水パフォ取れました。やったー!
ヒューリスティック・コンテストに関して
AHC 020 は初心者を意識したコンテストだったらしく、かなり ABC に寄せられています。制約は以下でした:
- 制限時間は 4 時間 (15:00 ~ 19:00 の開催)
- 提出のペナルティ無し、提出間隔は 5 分以上
使用したアルゴリズムは最小全域木と 2 分探索です。途中からスコアが頭打ちになり、 ABC とは違った壁を感じました。もう打つ手が無いぞ!
使用言語に関して
今回も AtCoder ガチ言語 Haskell で参加しました。 Tier 3 までの言語なら、大きな差は出ないように思いました。特にランダム性無しで解を求める際は時間制限を持て余す程です。
Common Lisp でも問題無く参加できそうですが、新たな病気に罹患するまでは Haskell 続投かなぁと思います。
今後
公式解説放送 を観たりサンダー本を読んだりしようと思います。今回は焼きなましだったらしいですが、適用方法が見えません。勉強します。
ABC をやらなければ一生 2 分探索しなかったでしょうし、 AHC をやらなければ一生焼きなめししないでしょう。雰囲気が分かる程度まではやってみようかと思います。
木 DP 再び (典型 90 問)
a -> [b] -> a
vs foldl'
+ s -> a -> a
)
部分技の畳み込み方法 (典型 073 の 公式解説 において、部分木の畳み込みが a -> [b] -> a
の観点で述べられていました。これは containers
の foldTree に 相当します (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 再び
foldTree
と foldTreeAll
のシグネチャを見直し、 典型 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 を引っ張り出してくるのが難しいなと思います。木において
参考になった色変記事
巷の入水記事では、よく『(狭めの範囲) を勉強したら入水できました!』となっています。それがいかに凄いか身にしみて理解していますが、自分の参考にはならないのです……!
逆にどういう記事が参考になったかメモしておきます。
どの記事も面白い (から読んでいる) のですが、参考になるかは別の尺度ですよね。
ピックアップ
-
【競プロ】新人SEがAtCoderを始めて水色になった【色変記事】
プレゼンが上手いです。まずタイトルが目立ちますし、中身を見れば隙きの無い実力を得たことが伺えます。 全部解けるようになれってことか……!
モチベーション面 で参考になっています。半年で水色、 1 年で青 というリアルタイム性が刺激的です。 Twitter も面白いし何もかも凄い。 -
AtCoder黄色を目指してやってきたこと
色辺記事にしては珍しく、主に 考察やライブラリ整備 に関して言及されています。マメに考えて自分に適用できると役に立ちそうです。
この方は解くのが遅いと言っていますが、 ABC で並走する限り、青色コーダーよりちょっと速いぐらいのスピードです。つまり僕からすれば爆速なのです。ギョギョーッ -
RubyとCrystalでAtCoder青色になりました
ライブラリ整備の具体例として、累積和が挙げられていました。csum.sum(l..r)
のようにアクセスするようです。僕は毎回図を書いて添字を考えていますが、その (認知的) 負荷が無くなる ということですね。上 ↑ の記事はこういう発見の重要性を伝えていたのかと非常に参考になりました。
感想
コツを見つけるのがコツだ!? そんな記事たちが刺激的でした。その他の記事も楽しく拝見しております。色辺記事を書いてくれた人の SNS は (忘れていなければ) 大体フォローしちゃう
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
関連- なお
ByteStringBuilder
はUnbox
ではないため、a -> ByteStringBuilder
を受け取ってVector a
を直接 fold します (map を挟みません) 。
- なお
- 基数ソート関連? (使い所は? 速い?)
app/Main.hs
haskell-src-exts で関数定義を取り出して、 Main.hs
に appendFile
する運用のようです。 AST を文字列に変換する際、それぞれの関数を 1 行に minify しています。
#define MOD
のようなマクロ定義はパース前に取り除きます。ファイルごとに #define
があるため重複するのと、マクロを消さないと Haskell としてパースできないようです (たぶん) 。
Data.Graph.Sparse
SparseGraph w
は CSR 形式 (隣接リストを 1 つの配列で済ませる形) のグラフです。重み付きグラフや木も同じデータ型で持つようです。
- 重さを別の Vector で持ち、
adj
とadjW
を両方提供する - Builer を切り出す
- RecordWildCard 言語拡張で destructuring ができる
- 添字は 1 次元限定
2 次元グリッドなどでSparseGraph
を使う場合は、手動で次元を変換しているようです。ここは盆栽の余地ありですね。
読みたいです
-
Data.Doubling
鉄則本とは少し違う? -
Data.BitSet
#
の嵐
分かりません
-
Data.PrimParser
モナディックパーサっぽいですが……?runSolver
と組み合わせると、とにかくmain
がカッコいいです。羨ましい。
いつか Haskeller に聞きたいこと
溜まったら特攻して聞きに行きます。
-
ソースのバンドリングの方法
cargo-equip 相当のツールを持っている? (つまりライブラリのソースをマージしてMain.hs
を生成している?) この際に minify したりIL(..)
を自動で付けている? -
可変長 vector
標準的なライブラリが無いと思うが、自作している? -
1 次元 vector への多次元 view
これはソースを読めば分かるはずですが、どれだったかな……? -
考えています……
Main.hs
の bundler を作る (1): 構想
自作ライブラリ (stack/cabal プロジェクト) からファイルを選び、適宜 Main.hs
に合成できるようにします。要は cargo-equip の Haskell 版、自分専用の cojna/iota/app/Main.hs を作ります。 ~200 行くらいの軽いコードになるはずです。
動機
HLS を動かしたいです。大きなソースで Template Haskell を使うと HLS が動かなくなり (泣), 変数の一括リネーム等が不可能になります。必要なコードのみ Main.hs
に取り込む形にすれば、 HLS も動くようになるはずです。
先駆者の動き
運用方法 (想像)
-
My.Prelude
のみをバンドリングしたMain.hs
をテンプレートとします。 - 随時
iota Data.Algorithm.BinarySearch a/Main.hs
のようにライブラリを追加していきます。
ソース構成
先駆者の Main.hs
は以下の並びです:
- 言語拡張
- Import
- main 関数
- 合成されたライブラリのコード
実装
言語拡張・ 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 ではなくテキストとしてパーツをパースするのが良さそうです。
- 僕含む Vimium ユーザが、
-
取り込まれたライブラリをアルファベット順でソートする
- たとえば
MyLib.Algorithm.BinarySearch
はMyLib.Prelude
よりも先に来るようにします。
- たとえば
CLI
- テンプレート (全言語拡張・Import + Prelude) の自動生成
- 全言語拡張・ Import は、ライブラリファイルの AST から抜き出したものの和です。
- ファジーファインダで合成するファイルを選ぶ
やらない (けどやりたい) こと
依存関係の自動解決
-
A
がB
に依存している場合、A
の合成時にB
も自動で読み込む - もしくは合成ファイルのセットをファジーファインダから選ぶ
提出コードを最小化する
- ソース提出前に、未使用のコード (関数・ Import) を削除する
- AST 上で未使用コードを探るツールが無いか
-
cabal
をライブラリとして使う -
cabal
(CLI) のエラー出力を使う
cabal
のエラー出力から未使用関数名を取り出す → AST から関数を削除する →cabal
のエラー出力から未使用 Import を取り出す → 未使用 Import を削除する?
Bundling 化を意識させない
- 自作ライブラリを直接
Import
可能とする
Main.hs
の中でimport MyLib.Algorithm.BinarySearch
と書いたら、提出時に自動で展開されます。もちろんMain.hs
の編集中は、自作ライブラリのコードにジャンプしたりできます。
Main.hs
の bundler を作る (2): 1 行に minify してみる
toy-lib
-
stack new
でプロジェクト作成 -
iota
のMain.hs
をパクって来て動かす -
rawSystem
でエラー。 NixOS のためかstack
が見えていない。。。
stack --no-nix-pure run .. で解決!!
あるいは stack.yaml
で pure を切っておきます:
# NixOS では Nix integration 有効がデフォルトらしいので、
# stack を見つけられるように `pure` を切っておく:
nix:
pure: false
2,500 行のテンプレートを 1 行に minify してみる
やってみました。 AtCoder 上では見やすいですね:
エディタで長い行を表示すると重い重い。。畳んでサクサク動くようになりました。コード自体は相当長いため、相変わらず LSP はセグフォで死んでいます。
toy-lib
ライブラリのリポジトリを作りました。 CLI でライブラリ全体を 1 行に minify します。
ファイル分割したおかげで、 haddock
がモジュール分けされるようになりました:
こんな感じだったのか〜。
ただファイル冒頭の『GitHub の README を見てくれ!』というコメントすら、何を言いたかったのか分からない……w ←
package.yaml
のdescription
デフォルト値でした。そこから取ってくるのか……
トポロジカルソート
モジュールの依存関係でソートしなければコンパイルに失敗する
Done
Unrated 対策下で常設コンにおける Haskell の提出を読むには
提出が多すぎて、クエリ処理が終わらないっぽいですね……!
- 👍: ユーザで絞り込む (mod_poppo 氏!)
- 👎: 言語で絞り込む (遅過ぎて弾かれる)
CSR 形式のグラフ
隣接リスト形式のグラフにおいて、すべての辺を 1 本の配列に納める方法に名前が付いています。 cojna/iota
の Data.Graph.Sparse
はこれですか。上手い!
Haskell 同人誌が読みたい
妄想です。
勝手にこういう画像作っちゃダメですね……
ドリームキャストはともかく、実際 Haskell で茶色コーダーになるための本があればいいなと思います。ループ処理やリストで FIFO など、『当たり前』だけれど経験値の溜まりにくい部分を、スラスラ読み流すだけで補える本があれば……!
グラフ典型メモ
やはりグラフが難しい。。水 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)
全頂点間の計算を で実施する。経由する 1 辺のみが結果を決める場合に有効……、、、O(N) -
LCA
レーティング 4 桁!
っしゃらい!
包除原理 レベル 1
忘れそうなのでメモしておきます。
包除原理とは
すべての積集合を元に和集合を求めるやつらしいです。 tsutaji さんのスライド の『初級』の部分を参考にしています。
包除原理の使い方
-
が奇数なら|\mathbb {k}| , 偶数なら- + -
のイテレーション方法\mathbb k - ビット全探索
-
毎にイテレートして、対称性を元に|\mathbb k| をかける{}_N C_{|\mathbb k|}
問題
-
ABC 304 F
AtCoder ABC304-F Shift Table を 2 通りの方法で解く の方針 1 は DP で普通に解けます。方針 2 の包除に帰結するやつは考察が無理だ……!
-
ベキ集合とか言ってシュッっと書きたいが…… ↩︎
MVector
, 爆速……!
2D ABC 307 C で Arrray
を MVector
に変えるだけで 763 ms の解答が 134 ms になりました。なんか知らんけどいいぞ……!
Data.Ix
が遅いとかvector
のストリームが速いとか?
2D MVectorへの view
MVector
を slice すると 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 を書く、ここに貼る
直後に AHC 021 に投下できて良かった
隣接行列 と行列累乗
概要
正方行列 1
, 張られていなければ 0
とする) 。
本当は walk の数らしい? 重さも載りそう
- 対角成分は自己ループを表すため注意 (大抵 0 のはず)
- 累乗計算はダブリングすると高速化できる
行列累乗 (mod) の計算方法
Array
mod_poppo 氏の提出 からパクりました。ただしaccumArray
よりも listArray
の方が速いです。
MVector
TODO
massive
TODO
対角化
TODO
問題
-
EDPC R - Walk
頂点数 遷移回数50 10^{18} -
ABC 212 E
頂点数 でダブリングすると TLE になりました。5000 くらいかかりそうだし。思案中…… → 参りました、、!N^3
EDPC / TDPC の難度
EDPC をある程度やると、水 diff DP がサクっと解けることもあります。グラフの EDPC 、転倒数の EDPC というような高難度問題セットがあれば、同じように高山トレーニングできて良さそうですが。
案外黄 diff 過去問を解くのがレーティング向上の最短ルートだったりして……? そんなことを考えながらも、本日も解説 AC をキメて行きます。
言ってる側から解けない DP が……
驕るには最後にします。半年間灰色レーティングだった日々を思い出せ…… orz
解き方メモ
任意の組み合わせが許される場合、 (全探索と言わず) 都合の良い順に貪欲に見ていけば処理できることもある
どうしたことだ、この Haskell の提出の数は……!
増えているッ……! 提出の種類が、圧倒的にッ…………!!
Twitter が見れない時も、 Zenn が Twitter なので平常運転だったりします。コミュニティガイドライン的にはよろしくないですが……
内部レーティング
AtCoder のレーティング計算式には『リセマラ補正』がかかっているそうです。
レート(第三段階)=レート(第二段階)−リセマラ対策補正
Type Checker で補正前のレーティングを確認できます。参加回数さえ増やせばとっくに色変してるじゃないか! という場合もあるようです。早々と色変する人は、 1 色上の実力を持っているかもしれません。
Stan (Haskell static analyzer)
久しぶりに Mac を開くと、 HLS から謎の lint が:
これは stan による提案 STAN-0210 のようです。
forM_
が遅くなりがち
古い GHC では 改善方法としては、モナディックループには loop を使えとあります。いや〜使いにくい。
当面は cojna/iota の stream
(vector
パッケージの monadic stream) 相当の関数を使います。 関連の Issue は既に閉じており、最近の GHC では改善が期待できそうです。
Data.Ix.range
に対する forM_
の高速化
Data.Ix
と vector
ベースのグラフ実装のため、 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
高速化関係
Libary Checker (Yosupo Judge)
AtCoder Library Practice Contest の強いやつという感じです。これはいつか制覇してみたいですね。記事のネタにもなりそうですし。ジャッジの仕組みも気になります。
なんとサイト自体に AC 表示機能がある。できれば最速 AC 時間と自分の AC 時間も表示してほしい。
online-judge-tools/verification-helper
oj-verify
は Library Checker と Aizu Online Judge の問題をローカルや CI でジャッジするためのツールのようです。実は AtCoder の問題もジャッジできるようで、なんちゃってベンチマークに使ってみたいと思います。
{- verification-helper: PROBLEM https://onlinejudge.u-aizu.ac.jp/courses/lesson/2/ITP1/1/ITP1_1_A
-}
-- ファイル冒頭のコメントで問題を指定できるっぽい
- Online Judge Verification Helper の細かい仕様
- 競プロライブラリに自動で verify をしてくれる CI を簡単に設定できる Web ページ
- HelloWorld.Test.sh
ACLにおける定数倍高速化
こたつがめ先生のスライドです。面白い内容がいっぱい。特にグラフを CSR 形式 で持つやつは最近僕も取り組んでいます。
array
モドキの n 次元 vector
cojna/iota の SparseGraph w は見通しが良いです。辺に重みがある場合、無い場合も同じデータ型を使用できます。アクセサが複数あり、重みが不要なら gr `adj` v
, 重みが必要なら gr `adjW` v
を使います。
作りたいもの
添字を Ix
に拡張した SparseGraph i w
を作ろうと思います。 内部的には、頂点は Int
型になるため常に Vector
を使えば良いです 。ただしユーザ側の API では Ix
型を使いたいです。そのため Ix
, Int
間の相互変換を提供する Unindex
型クラス、 array
より高速な array
モドキとして IxVector
, IxMVector
型を作ろうと思います。
以前の投稿
-
MVector をスライスして添字アクセスを n 回行う
2 次元用、 3 次元用のコードが分かれて良くない気がしました。 -
forM_ (range ..)
の高速化 (WIP)
少なくとも古い GHC ではforM_
の最適化が働きにくいため、 vector パッケージの monadic stream に置き換えます。 WIP
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 #-} による動作未確認
MArray vs IxVector vs View2d
ABC 308 D を 3 通り試してみましたが、全くスピードが変わりませんでした。 Vector にしたからと言って、無条件で速くなる訳でもないですね。以前の確認では、配列内の全要素を操作するストリーム処理が混じったために高速化できたのでしょう。
操作が限定される限り、 N 次元配列の実装は array で良いという結論になりそうです。ストリーム処理とかスライスが交じると IxVector 一択になるのかなと思います。まあ ixmap はできるけれど、それぐらいが array のメリットかなぁ……。
ひとまず View2d は削除します。
ヒュ
一度でも焼きなまし (SA: Simulated Annealing) をやってみなければ……! メモメモ
- Introduction to Heuristic Contest
- 鉄則本
- 直感でわかる、ヒューリスティック問題の羅針盤 ~貪欲法から山登り法まで~
-
競プロ始めました
一度でもヒューリスティックに参加すると、ブログの内容が非常に楽しい
TODO: ガバベンチ
本をあたる
地道に全部調べるのが大事なんですね! (あまりやっていない……)
- Algorith Design with Haskell 2 章
- Haskell High Performance Programming 3 章
- Haskell Optimization Handbook
ツールをあたる
ベンチマークを行う
- String vs ByteString
- N 回 print vs unlines
- N 回 print vs ByteStringBuilder
不採用: 無名関数経由で引数の順番を入れ替える
人の提出でこの手のコードを見ました。
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
を使っていこうかと思います。
Random
Modulus だと思っていたものが modulo でした……
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
によって添字をループさせたり回転させたり……いや探索済み頂点の管理を考えると、新たに配列を作ったほうが安全そうです。
What I Wish I Knew When Learning Haskell)
Prelude に古い部分がある (日本語版を読んでいたら Data.List
よりも Foldable
や Traversable
を使いなさいとあって愕然としました。僕の Haskell は、始めたときから obsolete だったかもしれません。確かにリストと vector
で別名の関数を使わなくてもいいよね……。
自作関数を foldFor
と foldForVG
のようにバージョン分けしていたのも全く無意味……というわけではなく、 GHC 8.8.3 相当の vector
には Foldable
の実装がありませんでした。 Prelude の植え替えは、言語アップデート後に検討します。
f . runST $ do ..
はなぜコンパイルできないのか
こうした事例 (playground) では謎のエラーが出ます。むしろ f $ runST $ do ..
の方に特殊な推論が働いているようです。へー……。こういう知識を経験則として覚えてしまうため、言語オタクは名乗れません。プログラミング言語は難しいよ……!
ModInt
に対する Enum
実装
Enum
を実装すると succ
, pred
が使えるようになります。 (+ modInt 1)
より succ
と書きたいわけで便利です:
instance Enum IntMod where
toEnum = intMod
fromEnum = coerce
なんで preceding value が pred
なのかと思いきや、 predecessor だったようです。 succ
も succeding value じゃなくて successor だったのか……。英語音痴だったかもしれない。
キューが無い件
リストを引きずり回して cons していますが、書きにくい上に FILO になります。自然なコードになるようリスト関係のモナドを使う/作るとなると、いっそ ST
モナドで可変 queue を使っても良い気がします。 mutable-containers や cojna/iota
の Buffer
の利用を真面目に検討して良さそうです。
-
State
モナドっぽい形 (Writer
?) の利点
引数からキューが消えます。 Haskell っぽいエレガントなコードになるかもしれません。弱点は、おそらくモナドの合成が必要になることです。StateT
を使いこなせる気がしない…… -
ST
モナドを使う利点
push 操作の引数にキューを指定できて明示的なコードになります。無難ですね〜……。
あー、ixmap 中で配列作ってるんですね。作ってないと思い込んでいましたw
vector
への考え
n 次元 区切りが付きました。
Int
で十分
グラフの頂点はフラットな 1 次元 - グラフの生成時に n 次元添字
i
を 1 次元整数の頂点Vertex
に変換しておく - DFS などの最中は常に
Vertex
にしか関心が無い - ただし逃げ道として
Int -> i
という逆変換 (Unindex
) も作っておくと土壇場で融通が効く
DP における n 次元配列はやはり必要
- これは n 次元添字越しのアクセスが頻発するため有効!
-
STUArray
を使ってもIxVector
(以前の投稿) を使ってもパフォーマンス上は大差無い
array
vs IxVector
)
n 次元配列として使うべきデータ型とは (-
IxVector
の方がイテレーションやスライスには強い -
IxVector
にはタプルやModInt
を始めとしたユーザー定義型が乗る - array を廃止した独自路線 (
IxVector
) で突き進んで良さそう
TODO: intsN
concat <$> replicateM n ints するよりも 1 回の unfoldr で済ませたい
- N 行読み取り (案外難しそう)
- 行ベースのパースを止めて、 Int ストリームから HW 個読み取る?
- 1 本の vector に入れてスライスするとか
8 問体制の水 diff を埋めました
けっこう解きました。要求知識は意外と緑 diff の問題から変化ありませんでした。盆栽を止めて演習に専念したためか、レーティングも順調に上がっています。
しかしここから水パフォ後半を 4 連発しないといけないのはキツいです。 RPG においても、硬い敵が強いって相場が決まってますよ、、!
これでレーティングが上がらなければ、何をやったらいいのか分かりません。結果が出て欲しいな〜と思いつつ、 PAST 上級とか典型 90 問を埋めていこうと思います。
あと 3 連発
全体で見ると、アクティブユーザ中の 9104 位だったりします。自分より上のプレイヤーが普段の ABC 参加者よりも多い。皆さん強いな……
いつも思いますが Difficulty Pies の茶や緑の解答数が、他の入緑した方や入水した方に比較してもかなり少ないですよね。その AC 数でここまでのパフォーマンスが出るのがすごいなと思ってます。
精進の仕方のアプローチが一般的なそれ (一定量の問題を解く) と違いそう & それよりもずっと効率が良さそうですね...!
いつもありがとうございます、、!
茶・緑 diff の問題を解いていないように見えますが、実は常設コンの問題 (PAST 過去問など) が反映されていないだけだったりします。
解き方としては (茶 → 緑 → 水 → 水) のような diff のローリングを繰り返しています。茶 diff の約数列挙が解けなかったりするので一長一短ですが、緑 diff が強くてニューゲーム状態になることもしばしばあります。実質的に高山トレーニングかもしれません。
今がレーティングの頭打ちってことは無いよね
人のレーティングを除くと、緑後半に留まるケースが多くて不安になります。これから 1 年停滞するのではないかと……
よしっ、適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff 適正 diff (素振り)
あと 2 回
またナップサック DP が出たり、レーティングの前提が崩壊したりで複雑な心境ですが、ついに色変が射程圏内に入りました。 PAST 上級も併せて狙っていこうと思います。
ARC で爆死しませんように、 ARC で爆死しませんように……
典型 90 問 ★ 4
埋めました。基本的な考察・実装を網羅した良問だらけで良かったです。僕の場合は、 01-BFS や整数問題が苦手であることがよく分かりました。
次々に AC できるのは最後の収穫の段階なので、それ以前のポテンシャルを高めるフェーズ、盆栽フェーズが重要だと感じました。 (外から観察する分には、解いた問題数から育てたポテンシャルも測れると思って良さそうです) 。
典型 90 問 ★ 5
埋めました。青 diff 級の問題も『水 diff です』という体で登場します。復習しないとですね。
TODO: 復習用問題リスト (ネタバレ)
- 029 - Long Bricks(★5) : 遅延セグ木
- 030 - K Factors(★5): 篩
- 036 - Max Manhattan Distance(★5): マンハッタン距離の性質
- 039 - Tree Distance(★5): 全方位木 DP
- 051 - Typical Shop(★5): 半分全列挙 (難しい……)
これからどうしよう
PAST 上級をやるか……!
-
vector
にconcatMap
があった、、!
ささやかなゲームチェンジャーです。リストでできることは、大体vector
でもできるということですね。 -
グリッド表示用 newtype
GridView
とかどうだろう
たま〜〜に役に立つはず! というかgridShow
でいいか……
constructN
DP 第一の類型: B - Frog 2 などは、畳込み (ナップサック問題など) よりも簡単な DP とされています。改めて解くと、 MVector
ベースの解答となり苦戦しました。
このような『配列を左から埋める』 (もしくは配る) 類の DP は、 constructN
を使うと簡潔に記述できます。 constructN
の概要は、『配列の確定した部分を参照して新しい要素を作る』というもので、かつて諦めた遅延評価にも通ずる所があります。
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) すらまったく抜ける気配がありません。常時このレベルだったのではないかと妄想し、ゾクゾクしています。シンプルに強い。こうなりたいものです。
constructN ちょうど使う機会があったので、使ってみました。
そして以下は IOArray による手続的実装。constructN の方が速い。
constructN
でボトムアップに構築できるのは良いですね! 速くて楽で IntMod も載っています。
僕は iterateN n 回配る DP を思い浮かべました (解いていません) 。 constructN の発想に慣れたいです。
updateWith では、 f
の適用結果を正格に評価すると速くなるかもしれません。 modifyArray'
がそうなってました。
この辺りあやふやで ! 星人になっています
さすがです、f の結果にBangPatterns つけたら爆速になりました...!
正直、そこまで影響するとは思っていませんでした……! この変更を自分のライブラリに取り込むのは気が重かったですが、 $! 演算子 で代替できて安心しました (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 むずかしい〜〜 (>_<)
ああー、 $!
使い所がよくわかってなかったですが、こういう時に使うんですね...
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.0
〜 groupBy
があります。現在の 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 がありました。
スタック
スタックにはリストを使います。
正確評価しなければサンクが溜まるやつ
『スペースリーク』で調べると色々ヒットしますね。
サンクが増えるのを防ぐため、蓄積値を引きずり回すというのも理解できるようになりました (逆に a + b + .. + recursiveFunctionCall の形だとサンクが貯まる) 。
でも複雑過ぎて認識できません……
TODO: 末尾最適化
まだ末尾最適化が分かりません。セグ木の非再帰実装とかは、 Haskell においても高速化に繋がるのでしょうか。
prescsan / postscan 系関数を理解した
T - Permutation | EDPC の図解を作ってみました。別解として、 prescan / postscan 系関数を使う場合を考えました。
計算方法
複数回画像を差し替えました
prescan/postscan には慣れません……。こんな感じで 26 枚描いて Zenn に投稿したいと思います。
プレゼンがすべて
E8 さんレベルでは無いにせよ、映え画像で埋め尽くしたいですね〜
試行回数の期待値 DP: メンタルモデルと計算式
期待値 DP の等式を理解したくて考えています。
ボツ画像
まだまだ整理中です。
ボツ画像
たぶんこんな感じ。。定義に戻れば大体何とかなりますね。
J - Sushi も完全攻略できる投稿を目指していきます。
Convex Hull Trick
TODO: 起きたらこれを最後まで読む
良過ぎる、、 Z - Frog 2 の解説も入っていて神過ぎます。。。ブログも凄いしライブラリも凄い。サークルの スライド一覧 もいいですね。
僕の盆栽は凡才だったか……。見上げ過ぎると首が折れそうなので、まずは CHT のスライドを実装してみます。
FPS とは..
DP が謎の計算で解けるそうですが、何も分からない……!
異常高速化
バッファリング、 mmap, 異常パース……! 底知れぬ強さがいいですね。
constructN
は単なる N 回の push_back
だった
A - Frog 1 を Rust で提出してみました 。イテレータに .rev()
があるのはズルいですね〜〜 (Haskell で迂闊に reverse
すると、コピーが発生して
Haskell vs Rust
けっこういい勝負です。
まさか Haskell に匹敵するとは……!! 恐るべし Rust ()
Haskell 以上に厳格なコードが書けることもしばしばあるようです (たとえば Vec
に Option
が載るため番兵が要りません) 。後発の良さが見えます。
ツイート
ABC 312 まで
-
Haskellで解くAtCoder - The curse of λ
真面目に書かれているだけにオチが面白い -
vector 13 の嬉しさとして認識できているもの
- derivingVia で
Unbox
実装 - iscanl' など添字付きイテレーションの追加
-
Traversable
,Foldable
の実装 (mapAccumL
が使える他、汎用的な関数が書けたり?) -
uncons
,groupBy
の追加
- derivingVia で
-
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
- 実はあるらしい。よっしゃ!、!!
- ネタ探し中
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 点毎の遷移を記述した解答が作れます。ただし畳み込みにより行毎に遷移を考えた方が筋が良いとは思います。
constructN
で解けた問題メモ
完全に自分専用スタイルとなりつつも、一旦メモ
-
巡回セールスマン問題 (の亜種):
dp[s][v]
main 上に可変配列や手続的ループが無いのは気が楽。パターンマッチでネストが深くなってしまうものの、when
などに比べればサクッと書けそうなレベルではある。
constructN
で解けそうな問題
-
J - Sushi は
constructN
で解けると思う -
区間 DP もデータの持ち方を
dp[len][offset]
の形にすればconstructN
で解けそうだし、より良い解法もありそう
ある問題を解くと別の問題も解けるようになることもある
EDPC で bit set に対する powerset
関数を手に入れました。解説 AC できなかった難問 ABC 310 D - Peaceful Teams を振り返ると、 powerset
関数を使った解答 であっさり AC できました。
Bit set を使うのはかなりズルという気もしますが、解ければ良かろうなのだ……!
メモ: リストに対する powerset 関数。ついぞこうした再帰関数を自力で書けるようにはなりませんでした。弱点をライブラリで補っており、実装力が足りていません。
言語アップデート 2023 の準備
2023/08/06 (日) の新ジャッジテストコンテスト (Algorithm, Heuristic) 以降、ジャッジ環境が刷新される予定です。前回 (Language Test 202001) から実に 3 年振りの更新となるようです。
リンク
-
使用できる言語とライブラリの一覧 (2023)
親方、ジャッジから Emacs Lisp が! - AtCoderの言語アップデートに関して 2023年版
- Language Test 202301
英語圏の人はこうした情報を拾えるのだろうか……
コンパイラバージョン
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) ../"
以上を参考にローカル環境をアップデートしていきます。
しまった、ビルドが終わらない
テストコンテストに参加したいんです頼む〜〜〜〜
しまった、ビルドが終わらない (2)
新環境で提出のビルドが長いのは完全に盲点でした。 cabal v2-build
が遅いのか、リンクが長いのか。
追記: 『ジャッジ初回実行時のウォームアップのために1~2分ほどステータスがWJのままになることがあります。』これが原因だと良いですが、流石に長い気もして不安です。
言語アップデート 2023 に対応完了
意外と変更がありません。デフォルトの言語拡張が増えて嬉しい程度です。
次回の ABC 以降は、過去の問題も 2023 仕様で提出できるようになる風です。
IsoUnbox
cojna/iota の更新で IsoUnbox を知りました。
As
を使うと、 vector
保存時に内部データ表現に変換できるようです。これなら sum type もタグの部分を Int にするなどして保存できます (また ニッチ最適化 で省メモリできるかも) 。
vector-th-unbox
と TemplateHaskell に別れを告げ、言語サーバーはクラッシュしなくなるのか……!?
HLS が動いた!
遂にクソデカテンプレートに対して HLS が動くようになりました! 大規模なリネームが簡単になったのはありがたいことですし、 変数の型を表示できるようになった のがデバッグで有利です。
ただ『関数型言語 Rust』の難しさが Haskell と似ていたことを考えると、そもそも関数型スタイルだと型が複雑になり、 LS の補助があってもわけが分からない可能性は高いです。
Unbox
の実装方法
Template Haskell を消せるようになったおかげで HLS が動いたと思います。 vector
13 において、 Unbox
の実装方法は 3 種類ありました:
-
単なる primitive の
newtype
である場合
UnboxViaPrime を使います。が、 2. との違いが分かっていません。今日のcojna/iota
で使われていないのを見ると、使わないほうが良いかもしれません。 -
newtype
である場合
Unboxed の冒頭の通り『GeneralizedNewtypeDeriving
』ができるようです。この拡張はGHC2021
で自動的に有効になります。 -
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 分かりませんだ……
atcoder-cli
しばらく前から最新の online-judge-tools
と組み合わせると動かなくなっています (lxml
が無いとかなんとか。おま環?) 。 online-judge-tools
単品だと動くので、もしかして oj
単体に乗り換えるべき……?
いずれ昨今更新がないので、私も最新だと思いますが動いています
Python の lxml のインストール周りは何かとよくはまりがちなので一度 python 環境を洗ってみると良いかなと思います。
atcoder-cli が参照している oj と、直接コマンド打った時の oj が違うもの見ててそれぞれの oj が別の python 環境に紐づいている、とかでしょうか...?
ありがとうございます!
atcoder-cli が参照している oj と、直接コマンド打った時の oj が違うもの見ててそれぞれの oj が別の python 環境に紐づいている、とかでしょうか...?
これをヒントに解決できました!!
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
検証: 水 diff を 100 問解けば水色コーダーになれる説
入水ならず! 青 diff が解けかかったかけれども及ばずでした!