Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rubyで競プロ用DSLを作ってみた wakate.rb#7

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →
Avatar for nola nola
June 22, 2026

Rubyで競プロ用DSLを作ってみた wakate.rb#7

Avatar for nola

nola

June 22, 2026

Other Decks in Technology

Transcript

  1. 👋自己紹介 🌱 のーら GMOペパボ株式会社 / 技術部 新卒 / Ruby初心者 🎨

    趣味 元競プロer (Python / 緑〜水) お絵描き ギター … 💎 今回 Ruby に入門 競プロを Ruby でやってみた Ruby は今回入門中 🐤
  2. 📖DSL とは? 🗣️ Domain Specific Language = 特定の用途向けに作られた 小さな専用言語 普通の

    Ruby コードではあるけど、その世界では一番自然に読める書き方 ✨ 🧪 RSpec describe User do it "is valid" do expect(user).to be_valid end end テストを「描く」DSL 💎 Gemfile source 'https://rubygems.org' gem 'rails', '~> 7.1' gem 'pg' group :test do gem 'rspec' end 依存を宣言する DSL 🛠️ Rakefile task :deploy => :test do sh 'cap production deploy' end task :test do sh 'rspec' end タスクを定義する DSL
  3. 🌊BFS とは? — 広く浅く探す Breadth-First Search (幅優先探索) 🌊 距離が 近い順

    に発見していく 📏 「最短ステップ数」を求めるのが得意 🧰 道具は キュー (FIFO) 訪問順: 1 → 2 → 2 → 3 → 3 → 3 → 3 同じ距離のノードに 同時に 波が届くイメージ 🌊 1 2 2 3 3 3 3 スタート 距離 1 距離 2
  4. 🌀DFS とは? — 深く一本道で進む Depth-First Search (深さ優先探索) 🌀 行けるところまで 深く進む

    ↩️ 行き止まりで 戻って 次の枝へ 🧰 道具は スタック (LIFO) / 再帰 訪問順: 1 → 2 → 3 → 4 → 5 → 6 → 7 同じグラフでも、訪問順は 一本道 ぐるーり 🌀 1 2 3 4 5 6 7 スタート 中盤 後半
  5. 🔮Ruby DSL の本質: instance_eval ブロックの中だけ self を差し替えられ る。 → start

    1 が self.start(1) として届 く ✨ Ruby は self. を省略できる → ブロック内の 見た目が「専用構文」っぽくなる 外側のローカル変数は 閉包 でそのまま使える これが DSL を Ruby らしく書ける正体 problem = search_problem do start 1 goal? { |n| n == 20 } transitions { |n| [n - 1, n + 1, n * 2] } valid? { |n| n.between?(0, 100) } end problem.solve_with(:bfs) problem.solve_with(:dfs)
  6. 🛠️作った状態空間探索 DSL 骨組みは instance_eval 1行。 start / goal? / transitions

    / valid? で問題を 宣言 solve_with(:bfs) / (:dfs) でアルゴリズム 切替 🧪 道具は RSpec で TDD 問題を解く前に、自分の道具をテストする module StateSpaceSearch class Problem def self.build(&definition) new.tap { |p| p.instance_eval(&definition) } end end end def search_problem(&) StateSpaceSearch::Problem.build(&) end
  7. 🏁ATC001-A: DSL が綺麗に書ける 💎 DSL版 problem = search_problem do start

    start_position goal? { |pos| pos == goal_position } transitions do |y, x| DIRS.map { |dy,dx| [y+dy, x+dx] } end valid? do |y, x| y.between?(0, h-1) && x.between?(0, w-1) && grid[y][x] != '#' end end puts problem.solve_with(:dfs) .reachable? ? 'Yes' : 'No' 💎 素Ruby版 queue = [start]; head = 0 visited = Array.new(h){Array.new(w,fals visited[start[0]][start[1]] = true reachable = false while head < queue.length y, x = queue[head]; head += 1 if [y,x] == goal reachable = true; break end DIRS.each do |dy, dx| ny, nx = y+dy, x+dx next if ny < 0 || ny >= h next if nx < 0 || nx >= w next if grid[ny][nx] == '#' next if visited[ny][nx] visited[ny][nx] = true 🐍 Python版 queue = deque([start]) visited = [[False]*w for _ in range(h)] visited[start[0]][start[1]] = True while queue: y, x = queue.popleft() if (y, x) == goal: print("Yes"); return for dy, dx in DIRS: ny, nx = y+dy, x+dx if not (0 <= ny < h): continue if not (0 <= nx < w): continue if grid[ny][nx] == "#": continu if visited[ny][nx]: continue visited[ny][nx] = True queue.append((ny, nx)) i 問題が「何か」だけ書く DSL 版 vs ループを自分で回す版
  8. ⏱️実行時間 — ATC001-A 🐍 Python (CPython 3.11) AC 126 ms

    提出 #76880813 / 100 pt 🥇 一番速い 💎 Ruby ( 素) AC 206 ms 提出 #76880815 / 100 pt 🥈 💎 Ruby DSL AC 848 ms 提出 #76880792 / 100 pt 🥉 DSL 税 Python が一番速い。Ruby DSL が一番遅い。
  9. 💥ABC213-E: DSL が汚れる 💎 DSL版 def free_closure(seeds, h, w, passable)

    problem = search_problem do start [:seed_list, seeds] goal? { false } transitions do |state| if state[0] == :seed_list state[1] else y, x = state DIRS.filter_map { |dy,dx| # 範囲チェック・通行可判定 ... } end end valid? do |state| state[0].is_a?(Integer) && passable[[state[0], state[1]]] end end problem.solve_with(:bfs).visit_order end frontier = free_closure([[0,0]], ...) seen = {}; punches = 0 loop do break puts punches if seen[goal] 💎 素Ruby版 (01-BFS) class Deque # 自前 deque (push_front/back, pop_fro end dist = Array.new(H) { Array.new(W, INF) dist[0][0] = 0 queue = Deque.new queue.push_front([[0,0], 0]) until queue.empty? (y, x), c = queue.pop_front next if c != dist[y][x] # コスト0: 4近傍の通行可マス DIRS.each do |dy, dx| ny, nx = y+dy, x+dx # ... dist 更新 + push_front end 範 🐍 Python版 (k回ラウンド) count = [[INF]*W for _ in range(H)] count[0][0] = 0 for k in range(H * W): # 距離 k の点から DFS で通行可を広げる todo = deque( (i,j) for i in range(H) for j i if count[i][j] == k ) while todo: vi, vj = todo.pop() for di, dj in [[0,-1],[0,1],[-1 wi, wj = vi+di, vj+dj # ... count[wi][wj] = k に更 # 距離 k+1 を 5×5 範囲で更新 for (i, j) in [...]: i DSL版だけ 外側のループ が DSLの外に漏れる。 「何を解くか」だけ書きたかったのに、結局手続きを書く羽目に
  10. ⏱️実行時間 — ABC213-E (今回も) 🐍 Python (CPython 3.11) AC 499

    ms 提出 #58076073 / 500 pt 🥇 一番速い 💎 Ruby ( 素) AC 1068 ms 提出 #76881020 / 500 pt 🥈 💎 Ruby DSL AC 2000 ms 提出 #76881018 / 500 pt 🥉 DSL 税 この順番は変わらない。DSL 税…!
  11. 🌸まとめ ✨ 単純な探索は DSL で綺麗 💥 複雑な更新は DSL の外に漏れ る

    🐢 DSL は一番遅い 💖 でも書く体験は楽しい 境界はあるけれど、Ruby に入門するきっかけとしては最高でした。 境界はあるけれど、Ruby に入門するきっかけとしては最高でした。 答えだけでなく、自分の道具をテストする。 境界がはっきりすると、毎日の競プロが少し楽しくなりました 🌸
  12. 🧪おまけ: Red → Green → Refactor の足跡 道具(DSL)は RSpec で

    TDD して育てた。 no-la/ruby-competitive-rspec の commit 履歴に、設計が育っていく歴史が残っている。 • RED Add failing BFS reachability spec • GREEN Define BFS search class • GREEN Return minimal reachable BFS result • RED Add unreachable BFS spec • GREEN Implement basic BFS reachability • REFACTOR Prevent revisiting BFS states • REFACTOR Track BFS visit order • REFACTOR Track shortest BFS distance ⋮ … (以降も同じリズムで続く) テストで育てた設計は、commit に履歴として残る ✨