/ 最近 .rdf 追記 編集 設定 本棚

脳log[20201111] 第四回 アルゴリズム実技検定



2020年11月11日 (水)

最終更新: 2021-02-20T23:02+0900

[AtCoder] 第四回 アルゴリズム実技検定

自分の提出第一回第二回第三回

今回は K 問題までたどり着けなくて、J 問題で詰まった。まだ解けていない。特定のカテゴリの入力が全滅しているから、見落としているパターンがあるし、それを除いてもまだ AC は遠そう。だけど ABC184の E 問題 Third Avenue は解けてるんですよ>20201122p01.03。不思議だなあ。

その後 K 問題は解けたし、なんなら L 問題も解けたけど、時間はかけた。そして M 問題。

 M 問題 筆塗り

まだ3つの TLE に阻まれている。

何について繰り返すか、その通りがかりに素早く答えを出すためにどんな最適化されたデータが準備できるか、というのが考えどころ。

実行時間の制限が長めの4秒ではあるけど、制約上の上限がどれもこれも 10^5 だから、何かについて繰り返しているあいだに探索やら配列埋めやら時間がかかる処理をするわけにはいかない。

たとえばクエリごとに色記録配列を埋めるとか、辺(=頂点集合を左右に分ける)ごとに関与するクエリを逆順に検索するとかは間に合わない。Ruby では間違いなく。

 提出 #18035280 (TLE×18 / AC×16 / 4419 ms / 234280 KB)

Array にはない ^ メソッドが使いたくて require 'set'; した。「対称差」を求めるメソッドらしい。しかし遅い。

 提出 #18036165 (TLE×9 / AC×25 / 4445 ms / 1224160 KB)

3つの演算子(+, -, &)を使って自分で Array#^ メソッドを実装した。+ の代わりに | メソッドを使うこともできるが速くなる気がしない。まだ遅い。

 提出 #18053537 (TLE×3 / AC×31 / 4416 ms / 210696 KB)

bsearch_index と concat で Array#^ メソッドを実装した。演算子を使ったシンプル実装より必ずしも速くなるわけではないが、遅くなるのは TLE になるほどではない小さいケースだし、直線に近い(色リストが長くなりやすい)木では有利になるためか TLE が減った。しかし残った3つの TLE (random_17.txt, random_19.txt, random_28.txt) はどの提出に対しても実行時間が上限に張り付いたままで、打開するヒントが見えない。どういう形の木なのか。

 方針
  1. 根を1つ決めて、木全体を葉から根へ遡るように処理する(なぜか根が上)。
  2. ノードは色リストを持っている。色は z-order で識別する。z-order は 1 から Q の範囲の値をとる。
  3. 子が持つ色リストを親のものにマージしていく。マージする過程でペアができればリストから取り除く。
  4. ノードが持つ色リストのうちもっとも手前にある(z-order が大きい)色が、そのノードと親を結ぶ辺の色となる。

N 個のノードについて繰り返しながらソート列(色リスト)のマージを繰り返すのが、時間的にもメモリ的にも厳しい。だけどこれが嘘のように時間制限をクリアできる魔法があるとも思えない。十分にストレートで迷いのないコードだと思う。この方針のままなんとか滑り込みたい。

「打開するヒントが見えない。どういう形の木なのか」と書いたけど、直線の反対ということで子供が 100, 1000 あるような木を用意したらてきめんに遅くなった。どうしようかな。

 提出 #18059514 (TLE×2 / AC×32 / 4418 ms / 201508 KB)

変更点はソート済み配列から最大値を取り出すのに max メソッドを使っていたうっかりの訂正と、子の色リストを得るのにランダムアクセスをやめてスタックから連続する領域を取り出すようにしたこと。これで1減って残る TLE は2個。

4400 ms が 4200 ms になったりするとあとちょっとだとわかるんだけど……。

あるノードに合流してきた色リストは LCA と z-order がともに昇順になるように並べ替えることができて、そこから外れる色は色塗りの出番がなくて無視できるんだけど()、それで良くなるものか……。

z-order が大きければ他の色より前に出られる。LCA が小さければ前にある色が LCA に到達して退場するのを待てる。どちらでもなければ前に出る前に退場させられる。

 提出 #18069462 (AC / 1394 ms / 106192 KB)

やった!!!

LCA が正解だったみたい。木を遡りながら色リストをマージするのは同じだけど、LCA を利用してリストを短くしたら TLE が解消した。

うれしい。とってもうれしい。ここ2、3日トイレでもお風呂でもふとんの中でも考えていた。あっさり AC されているとやる気が萎えるので見ないようにしていたが、Ruby で AC 一番乗り。

ところで PyPy3 のこのシンプルかつ Python 系で一番速い提出 #18047488 (819 ms) はなんの魔法だろう。半分以上が入力の処理で、残りでヒープの出し入れをしているだけ。

JIT で速いから余計な手をかけないで済むということなら Ruby には関係のないことだけど、考察が足りていなくて自分のアルゴリズムがヘボだというなら、学ばなければいけない。

それはそれとして、LCAの確定を待つ色のリストはソート済み配列をやめてプライオリティキューにすべきだし、lambda M の中の Array#slice! と Array#insert は配列に相応しいメソッドではない。配列を酷使しすぎている2点に改善の余地がある。

 提出 #18102965 (Crystal / 559 ms / 56432 KB)

初めて Crystal を書いた。色々言いたい。

  • Web で見られるランゲージリファレンスがない。ドキュメントがあることとアクセスしやすいことは必須。リファレンスだけあればいい。getting started はいらない。
  • Ruby を知っているからこそ罠が多い。
    • delete_if と inject メソッドがない。reject! と delete_if は完全に同じではないし、reduce と inject がただのエイリアスなら用意しないのは罠でしかない。

      inject は配列の各要素のあいだに演算子を挿入するイメージのメソッドらしい。それを読んでから inject の仕様に悩んだことはない。

    • lambda メソッドがない。テキトーに Proc.new と書いてみたけどダメだったので -> () {} と書いた。アロー記法は好きではない。

      パラメータには型注釈が必須だったけどコロンの前か後ろか両方にスペースが必要だったらしい。詰めて書いたら「ダメだ。我は ) を所望する」と怒られ、素直に ) を書いたら型注釈を付けろと怒られる無限ループ。

      後付けするなら記号は選ばなければいけない。Ruby の仕様(メソッド名と文字リテラル)なら条件演算子はむしろないほうがいいし、だけど条件演算子とシンボルリテラルは現実に存在しているし、記号は選ばなければいけない。

    • each メソッドが self を返さない。なぜ?
    • ブロック変数名に _ が使えない。なぜ?
    • 定数に多重代入ができない。なぜ?
    • 定数と変数が同じスコープ、見た目通りの実行軸にないように思う。

      n,q = gets.split.map(&.to_i)
      N = n; Q = q

      これは変数 n が定義されていないというエラーになった。

      (追記:下の方で答えを書いてたかも。「map が遅延評価? .to_a を付けないと期待したタイミングで副作用が起こらない。」 map が遅延評価であることと代入が行われないことは別だと思うのだが違うのか。違う(別ではない)なら大変に興味深い Crystal の特徴だと思う)

    • &:to_i と書いていたものを &.to_i と書かせる。

      .to_i が Crystal という言語を構成する部分として定義されており、プログラマが操作可能な対象であるなら評価は変わるけど、単に & という目印のバリエーションとして &. を追加したなら、ただの好き嫌いでただの罠。

    • ブロックパラメータは必ず1つ? {|i,|} とか {|i,j|} とかできない雰囲気。それとも StaticArray だったから?
    • map が遅延評価? .to_a を付けないと期待したタイミングで副作用が起こらない。
    • Enumerator のチェインができない。具体的には map.with_index と書くと map にブロックパラメータが必須だというエラーになった。スペックを読んだら map_with_index というメソッドがあったので使ったが、map だけ?
    • nil が非常に煩わしい。Ruby はすべての変数が Nullable だし、偽と評価される値が nil と false だけなのもあってあらゆる部分に nil が現れる。そのすべての nil に対応を迫られる。一例が gets.to_i とは書けなくて gets.to_s.to_i と書かされるところ。たぶん (gets||"").to_i でもいいとは思う。

      実行時エラーをコンパイルエラーにしたいのだとしよう。だけど視野が狭い。こちらは gets が nil を返すかどうか、そのような入力が与えられるかどうかに関する知識を持っており、nil が返るような入力が仕様違反であることを知っている。バグがあったときに修正すべき対象は Crystal のソースコードではなく、そのような入力を与えた外部にある。事の決定権はコンパイラにはない。

      &. によるメソッド呼び出しに対応していれば話は違った。

    • 空の配列から pop できない。空の配列を reduce できない。これらも nil がらみであろう。
    • 空の配列、空の Hash の作成がめんどくさい。[] とか {} とか書けない。とりあえず領域を確保するために [nil]*N と書くのもタイプミスマッチで都合が悪い。
    • "1(スペース)2".to_i がエラーになった。1 を期待していた。0 をデフォルトとして解釈できるところまで解釈することを .to_i には期待している。

すべてドキュメントの不足が悪い。スペックしか頼れるものがなかった。


俺は人類の手には多少余るとしても、プログラマを信頼し、力を与えてくれる言語が好きだ。安全のためと称して枷をはめようとする言語は選ばない。安全な(なまくら)は退屈だ。

では Ruby の切れ味は? Ruby はプログラマがやりたいことの邪魔をしないのがいい。Ruby がコードゴルフに向いているということとも関連する。キーワードやら形式やら、本筋の処理と無関係でありながら書かなければいけないお約束が少ないということ。

Crystal の型はどうか。プログラマに力を与えるためではなく、処理系が力を得るためにプログラマが受け入れる枷だというのが、少し触っての印象。枷を受け入れるなら C++ が選択肢に入る。

const 教の信者というのは最も“const と書きたくない”人種のことだと自認している。const という当たり前のことを、どうしてあちこちそちこちに書いて回らなければいけないのか。所有権も魅力的だけど、const と書かなくてもいいという1点でまず、Rust の評価が高い。

C++ も Ruby も好きだけど弱点がある。Rust には期待ができる。でもコンパイラが起動できないんだよなあ。「rustc.exe - エントリ ポイントが見つかりません。プロシージャ エントリ ポイント K32EnumProcessModules がダイナミック リンク ライブラリ KERNEL32.dll から見つかりませんでした。」 本でも読むか。

 別解@2021-02-20 提出 #20275440 (AC / 1168 Byte / 1081 ms / 73084 KB)

#18069462 と比べて、-1016 Byte / -313 ms / -33108 KB。短くて速くて省メモリ。

先に書いたように、「クエリごとに色記録配列を埋めるとか、辺(=頂点集合を左右に分ける)ごとに関与するクエリを逆順に検索するとかは間に合わない」のはたぶん間違いないけども、2つを混ぜてクエリの逆順に効率良く色記録配列を埋めるところまでは考えが及んでいなかった。

時間軸を反転することで、一度塗ったところを再度塗らないでスキップしたい

その方針でやってみれば、前半はほとんど同じことをやってるんだけど、下準備を終えたあとのメインループでは、ソート済み配列をマージする代わりにスキップしながら親をたどっていた。親をたどる方が短く書けて簡単!