見出し画像

PaizaでBランクを取るには「問題理解」「分解」「抽象化」が必要そう、という話

今週は風邪でダウンしておりました。
そこそこ復活しましたが3キロ痩せました。
※たまたま行った小さな医院で検査されなかっただけで、周囲の人間の状況的にはたぶんコロナっぽいです。そのため、完全に引きこもっておりました。コロナ水準ではかなり軽症でした。(MAX38.5°C)

しかし、いちど寝込むと、
考える体力も座る体力も、ペンを持つ握力すらも衰える気がするので良くないです。体調管理には本当に気をつけましょう。


PaizaでBランクが取りたい

病み上がりわたくし、Paizaに戻ってきました。
Bランクが取りたいのです。

これまでの記事でも時折書いてきましたが、「プログラミングの細かい書き方は、都度調べたら良い」という信念のもと、半年間通ったスクールの教材では、外的なリソースをかなり活用しながらこなしてきました。

AIの回答丸写し、みたいな「しょうもないこと」はしていませんが、
それでも「〇〇したい時、僕ならこう書くけど、もっといい書き方ある?」というふうにAIに質問して、いい感じのコードをポイっと出してくれるような使い方をしています。

で、スクールを卒業してから基本情報技術者試験に向けて学習しているのですが、並行してpaizaでプログラミングの地力をつけていこうと思ってちょくちょく勉強しています。

paizaのBランクはどの辺が難しくなるのか

さて、paizaというのはプログラミング学習サイトでもありプログラマの転職サイトでもあり、「プログラマの戦闘力をある程度、定量的に可視化してくれる」ようなサービスです。
どのくらいの難易度の問題を、おのれの実力だけで解けるか?というシンプルなシステムでランクが分けられていて、僕はいまCランクにいます。

今の僕から見えている範囲で、ランクを言語化すると…

  • Cランク ・・・ 簡単なアルゴリズムをコードに起こせる人

  • Bランク ・・・ やや複雑なアルゴリズムを俯瞰して、整えて、コードに起こせる人

という表現になります。詳しく見ていきます。

書かれてる順に対応してもうまくいかなくなる

ざっくりわかりやすくするために、算数の文章題のようなものを例えに出してみます。

Cランクでは、以下のような感じ。

1から始まって、2, 3 … と大きくなっていく数値を、10まで足した合計値はいくつでしょう?

答え:
1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55

一方で、Bランクでは以下のような感じです。

縦20行、横20列の客席をもつ映画館があります。
5行目の5列目にあなたが座っているとき、空いている一番近い座席を求めてください。ただし、以下の座席は埋まっているものとします。
…(埋まっている座席番号が列挙される)

おそらく、この問題では座標とか三角関数とか、何やらいろいろな方法を用いて目的を達成する必要がありそうです。どこから手をつけていいやら、一見よくわかりません。

このようにBランクとCランクの大きな違いは、「読んだ問題文を文字通りの順番でコードに起こしていけるかどうか」だと感じました。
Cランクの文章では、文章を読み進めるのと同じ方向と順番でコードを書いていけばどうにかなることが多いです。
(1から順に1ずつ増えていく数値を定義して、それを足していくような)

一方で、Bランクの問題では「どこをどう表現していけばよいのか」というように、問題を俯瞰した考え方を要求されます。

難しいメソッドやアルゴリズムを「知っている」こと自体も重要なのですが、それよりも「与えられた条件を満たして要件を叶えるために、問題を噛み砕いてコードに落とし込んでいく」という全体を俯瞰した「問題理解」、そこからの「使える知識を取り出してきて当てはめてみる」という作業になる感覚です。

CランクとBランクでは「実力の質」が違う

Cランクまでは、「自然言語をコードに変換・翻訳できる」という感覚だったのに対して、
Bランクでは「自然言語から問題を抽出し、分割し、適切に抽象化し、細かくした問題の各部位を適切にコード化し、再構築する」といった能力が必要になっている感覚です。

で、これはまさにプログラマに求められている能力でしょう。「こういう入力があったら、こうやって吐き出すコードを書いてくれ」って言われて、その通り書くだけのコーダーではやっていけないわけです。
(生成AIに真っ先に食われる人になりそうです)

一方で、「こういう状況で、なんとかこういう(理想)にしたいんですよねぇ」といった「お悩み」ベースの状況から、問題を俯瞰して分割し、適切に抽象化して解決策を探す筋肉こそが、結局今後のプログラマには求められるのかなぁ。 と感じました。

WETでも一旦書いてみる

Bランクの練習問題にトライしているのですが、なかなか鮮やかな解法にたどりつかないことも多いです。
泥臭く場合分けをして、めちゃめちゃ冗長なコードしか書けないと感じることもあります。

現状、一旦WETでも書いてみることにしています。
それでとりあえず動いたら、DRYにする方法を探したり、調べたりしてみます。

※WET というのは Write Everything Twice あるいは Write Every Timeの略で、何回も同じことを書いている様を指します。
DRYは "Don't Repeat Yourself" で、「同じことを2回書くな」ですね。

たとえば上の問題に対する、僕の回答はこちらです。

# グリッドの大きさを定義
h, w = gets.split.map(&:to_i)

# グリッドを格納
grid = []
h.times do
    row = gets.chomp.chars
    grid.push(row)
end

# 変更の起点になる座標を取得
y, x = gets.split.map(&:to_i)

# 最初の値を格納しておく
current_char = grid[y][x]

def flip_char(char)
    char == '.' ? '#' : '.'
end

def valid_coordinate?(y, x, h, w)
    y >= 0 && y < h && x >= 0 && x < w
end

# 縦を処理
h.times do |i|
    grid[i][x] = flip_char(grid[i][x])
end

# 横を処理
w.times do |i|
    grid[y][i] = flip_char(grid[y][i])
end

# 斜め(左上)を処理
cy = y
cx = x
while valid_coordinate?(cy, cx, h, w) do
    grid[cy][cx] = flip_char(grid[cy][cx])
    cy -= 1
    cx -= 1
end

# 斜め(右上)を処理
cy = y
cx = x
while valid_coordinate?(cy, cx, h, w) do
    grid[cy][cx] = flip_char(grid[cy][cx])
    cy += 1
    cx += 1
end

# 斜め(右下)を処理
cy = y
cx = x
while valid_coordinate?(cy, cx, h, w) do
    grid[cy][cx] = flip_char(grid[cy][cx])
    cy -= 1
    cx += 1
end

# 斜め(左下)を処理
cy = y
cx = x
while valid_coordinate?(cy, cx, h, w) do
    grid[cy][cx] = flip_char(grid[cy][cx])
    cy += 1
    cx -= 1
end

# 中心を処理
grid[y][x] = flip_char(current_char)

grid.each do |row|
    puts row.join
end

これも諸々ググりながら作ったコードなので、このBランク相当の回答ができたとしても、まだ自分がBランク相当であるとは言えません。
また、上記のコードは泥臭いことこの上ないです。力技にもほどがあります。でも動きます。

模範解答もそんなにエレガントじゃなかった

でも、ちょっと面白いことに、模範解答を見てもこのコードとやっている事があまり変わりませんでした。
要は、模範解答であっても、縦横斜めをそれぞれ場合分けしてゴリゴリ計算するコードが紹介されていたのです。

つまり泥臭いWETなコードと、「要件を満たすにはそれしかない…!」なコードの間には明確な国境があるわけではなく、実際のところそれで落とし所とするしかない。といった場面も往々にしてあるのでしょう。
たぶん。

美しいコードばかり追い求めても仕方ない。
「ひとまず動く」と「完璧に美しい」の間で、どこまでスピーディに「一旦OK」の解を出せるか?というのも、プログラマとしてのパフォーマンスの1つかもしれないと感じました。

AI氏の回答はエレガントすぎた

ちなみに、この問題についてもっとエレガントな解決策がないか?
と、僕のコードと問題文を与えてClaude氏に聞いてみたところ、以下の出力が得られました。

# 盤面のサイズを読み取る
h, w = gets.split.map { |num| num.to_i }

# 盤面の情報を格納する2次元配列を初期化
grid = []

# 盤面の情報を読み取る
h.times do |i|
  row = gets.chomp.chars
  grid.push(row)
end

# 変更する中心座標を読み取る
y, x = gets.split.map { |num| num.to_i }

# 文字を反転する関数
def flip_char(char)
  char == '.' ? '#' : '.'
end

# 座標が有効かどうかをチェックする関数
def valid_coordinate?(y, x, h, w)
  y >= 0 && y < h && x >= 0 && x < w
end

# 指定された方向に沿って文字を反転する関数
def flip_in_direction(grid, y, x, dy, dx, h, w)
  ny, nx = y + dy, x + dx
  while valid_coordinate?(ny, nx, h, w)
    grid[ny][nx] = flip_char(grid[ny][nx])
    ny += dy
    nx += dx
  end
end

# 中心座標の文字を反転
grid[y][x] = flip_char(grid[y][x])

# 8方向(上、下、左、右、左上、右上、左下、右下)に対して反転処理を行う
directions = [
  [-1, 0], [1, 0], [0, -1], [0, 1],  # 縦横
  [-1, -1], [-1, 1], [1, -1], [1, 1]  # 斜め
]

directions.each do |dy, dx|
  flip_in_direction(grid, y, x, dy, dx, h, w)
end

# 変更後の盤面を出力
grid.each do |row|
  puts row.join
end

これ、何やってるかわかりますか?

僕のコードでは「1マイナスする」とか「1プラスする」をそれぞれ場合分けしてハードコードしていたのに対して、AI先生は配列directionに8種類の方角を表す数字の組み合わせを持たせて、それを「加算していく」ことで、1つのflip_in_directionメソッドで処理できるようにしています。わーぉ。

つまり言い換えると、「プラス1を足す」の時と「マイナス1を足す」の時があることに注目して、「〇〇を足す」の部分をメソッド化して、
その〇〇の部分を配列で全パターン網羅しておき、配列内の全要素に対してeachを用いて処理しているというわけです。

これ、確かにDRY・WETの文脈ではエレガントなのですが、可読性とか保守性という意味ではちょっと不安を感じます。
とはいえ現時点では理解するのがやっとなので、参考程度に見ておきます。

おわりに

寝込んでいた期間で学習習慣が吹き飛びかけたので、再構築するのに苦心している土日です。
病み上がりの脳が考えることを放棄したがるようで、頭痛もするので
あえて説明するように文章を書く事で、自身の理解へと繋げるために本記事を書きました。
なんか、同じようなことをWETに書いているような気がする…という自覚はちょっとあるのですが、乱文お許しいただけると幸いです。

今日はここまで。最後までお読みいただきありがとうございました。

この記事が気に入ったらサポートをしてみませんか?