はじめに
paizaBランク(難易度2000弱)をスシで解いていきます。sushiです。日本人らしさがあって良いですね。言語はJavaScriptを使うので、WEB制作系を目指している駆け出しエンジニア向けの記事になります。
paizaBランクを取るメリット
週3件ほどスカウトが来る
プロフィール未入力の私のアカウントでさえ、毎週スカウトBOXがパンパンです。
駆け出しエンジニア時代は、未経験だと雇ってもらうのが大変だと思いますが、スカウトなら比較的求人にマッチングしやすいのではないでしょうか。ちなみに、私の所属する企業でも、普通にpaizaで求人出して面接など実施しています。
数字で評価されるので自信が付く
paizaレーティングという数字でスキルが常に可視化されます。
数字が大きいほどつよつよエンジニアというわけです。
ふわっとした感覚ではなく、明確な数字で評価してもらえるので、
高いスコアが取れればそのまま自信になります。
paizaBランクの難易度
3人に1人は解けるようなので、偏差値55とかそれくらいだと思います。
Bランクはスキルとか偏差値どうこうと言うより、
知るべきことを知っていれば問題なく解けます。
スシでも解けます。
paizaBランクを解くために必要なこと
1. 配列関数を覚える
一番重要です。一番難しく感じる部分でもあると思いますが、スシで解説するので大丈夫です。小学生でもわかるように説明していきます。
配列関数:split, push, slice, join, forEach, filter, map
2. 冗長でも正解できるコードを書く
普段の業務ではコードは綺麗に書くに越したことはありませんが、ただBランクを取るためだけであればあまりコードの綺麗さにこだわる必要はありません。代わりに、「正解できること」に徹底的にこだわりましょう。
テストケースを1つでも落としてしまえば、基本的にBランクに昇格することはできません。
3. 焦らず問題文の解読に時間を使う
paizaは問題の解読に平均回答時間の50%の時間を割くべきだと思っています。コーディングとデバッグは残りの時間で十分間に合います。
Bランクの問題はコード自体はかなり簡潔に書けることが多いため、テストケースの考慮漏れを防ぐために、問題の解読に時間を多く割いた方が高いスコアを取りやすいです。
スシで解説
早速始めていきましょう。
配列関数を覚える
まずは配列関数を7つ覚えてください。
1. split
split(スプリット)は、スシを分割するのが得意な器用なスシ職人です。
さて、皆さんは高級料亭paizaにてスシをフルコースでふるまわれるのですが、驚くべきことに、寿司職人paizaさんはいつも『シャリが連結したスシ』を提供してきます。
"卵鮭卵"
この状態のスシはとても食べづらく、せっかくの高級スシが台無しの状態です。しかし、こんな時に**split(スプリット)**という関数が活躍します。
let sushi = "卵鮭卵";
let sushi_split = sushi.split("");
console.log(sushi_split);
出力は以下のようになります。
["卵","鮭","卵"]
これでスシの境目が明確となり、さらにスシたちはスシゲタの上にお上品に並べてもらうことができました。
しかし、いじわる職人paizaさんはシャリを連結させるだけでなく、その間にバランまで挟んでくることがあります。
"卵バラン鮭バラン卵"
しかし、器用さに定評のある職人splitさんはこの程度では屈しません。split関数には職人の力を込めることができるスロット**(引数)**が存在し、これを活用すればこのバラン攻撃をかいくぐることができます。
let sushi = "卵バラン鮭バラン卵";
let sushi_split = sushi.split("バラン");
console.log(sushi_split);
["卵","鮭","卵"]
文字列.split("セパレータ")
とすることによって、何をもって配列の要素を区切るかを決めることができます。今回はスシとスシの間にバランが挟まっていたので、バランをセパレータとして指定することによって、スシだけを配列の要素として区切ることができました。最初に解説した文字列.split("")
のようにセパレータを指定しない場合は、与えられた文字列を1文字ずつ区切った配列が生成されるようになっています。
ちなみに、文字列.split("バラン")
のように書いたとき、splitの後に続くカッコの間の部分をその関数の**引数(ひきすう)**と呼びます。職人の力を込めることができるスロットのことです。この場合の引数は"バラン"
になります。
2. push
push(プッシュ)は引数で指定したスシを追加する素朴なスシ職人です。指定したスシをスシゲタの上に追加することができます。
let sushi = "卵鮭卵";
let sushi_split = sushi.split("");
let sushi_push = sushi_split.push("鮭");
console.log(sushi_push);
["卵","鮭","卵","鮭"]
3. slice
slice(スライス)はパワフルなスシ職人です。スシゲタの上に載せたスシをスシゲタごと出刃包丁で切り落とします。切り落とされたスシは、スシゲタごと消滅します。引数に与えた番号より前のスシを切り落とします。
let sushi = ["卵","鮭","鰯","エ"];
let sushi_slice = sushi.slice(1);
console.log(sushi_slice);
["鮭","鰯","エ"]
sliceには引数を2つ与えることも可能で、その場合は出刃包丁が二刀流になります。2つの出刃包丁の間のスシだけが生き残ります。
let sushi = ["卵","鮭","鰯","エ"];
let sushi_slice = sushi.slice(1,3);
console.log(sushi_slice);
["鮭","鰯"]
4. join
join(ジョイン)はいじわるスシ職人paizaさんの別名です。paizaさんを召喚し、スシのシャリを結合させます。バランを挟むことも可能です。
let sushi = ["卵","鮭","鰯","エ"];
let sushi_join = sushi.join("");
console.log(sushi_join);
let sushi_join_baran = sushi.join("バラン");
console.log(sushi_join_baran);
"卵鮭鰯エ"
"卵バラン鮭バラン鰯バランエ"
5. forEach
forEach(フォーイーチ)は回転ずしのベルトコンベアです。forEachは高級回転ずし店のベルトコンベアなので、同じスシは1回しか回ってきません。また、スシを回すだけでなく、回ってきたスシを確認していろいろな処理を行うことができます。
let sushi = ["穴","エ","鮪","鰯","卵"];
sushi.forEach(sushi_name => {
if(sushi_name == "鰯") {
console.log(sushi_name + ": イワシが回ってきました!");
} else if(sushi_name == "エ") {
console.log(sushi_name + ": エビが回ってきました!");
} else {
console.log(sushi_name + ": イワシ、エビ以外が回ってきました!");
}
});
"穴: イワシ、エビ以外が回ってきました!"
"エ: エビが回ってきました!"
"鮪: イワシ、エビ以外が回ってきました!"
"鰯: イワシが回ってきました!"
"卵: イワシ、エビ以外が回ってきました!"
6. filter
filter(フィルター)は注文が可能な回転ずしのベルトコンベアです。使い方はforEachと似ていますが、注文する条件を書いてreturn(リターン)という処理をすると、注文したスシだけが入った配列が返ってきます。
let sushi = ["穴","エ","鮪","鰯","卵"];
let sushi_order = sushi.filter(sushi_name => {
return sushi_name == "エ" || sushi_name == "卵";
});
console.log(sushi_order);
["エ", "卵"]
7. map
map(マップ)はスシ職人が途中に立ってるベルトコンベアです。使い方はforEachと似ていますが、回ってくるスシに手を加えることができます。たとえば、スシにわさびを入れる場合は以下のように使います。
let sushi = ["穴","エ","鮪","鰯","卵"];
let sushi_sabiiri = sushi.map(sushi_name => {
return sushi_name + "(サビ入り)";
});
console.log(sushi_sabiiri);
["穴(サビ入り)","エ(サビ入り)","鮪(サビ入り)","鰯(サビ入り)","卵(サビ入り)"]
以上、7つの配列関数を説明しました。
簡単におさらいしておきます。
- split スシを分割するのが得意な器用なスシ職人。文字列を配列化できる。
- push スシを追加する素朴な職人。配列に要素を追加できる。
- slice スシゲタごと切り落とすパワフル職人。元の配列の一部だけを取り出す。
- join スシのシャリを結合させるいじわる職人。配列を文字列化できる。
- forEach 回転ずしのベルトコンベア。配列の各要素を使ったループ処理ができる。
- filter 注文可能な回転ずしのベルトコンベア。returnで条件指定した要素だけの配列を返す。
- map スシ職人が途中に立ってるベルトコンベア。returnで返した要素が元の配列要素と置換された配列を返す。
実践:paizaBランク問題を解いてみる
ここまでの知識ですでにpaizaBランクを解くことができるようになりました。早速、Bランクの問題を解いてみましょう。まずは問題のページにアクセスします。アカウントを持っていない場合は、アカウントを作成してログインしてください。
ページにアクセスすると、ページ下部に解答欄があります。今回はこの解答欄に直接コードを打ち込んでいき、『提出前動作確認』というボタンを押しつつコーディングとデバッグを進めていく形になります。
それでは早速、問題を解いていきましょう。以下のような文字列が与えられるので、これを図形としてみたときに線対象、または点対象、またはその両方の場合、そうでない場合の4パターンを判定してください、というのが問題の内容になります。
2 3
###
...
まずは、上記の入力例というのがスシ職人paizaさんからどういう形で提供されているのかを確認してみましょう。14行目に書かれているlines[0]
という記述をlines
に置き換えて、提出前動作確認ボタンをクリックします。
process.stdin.resume();
process.stdin.setEncoding('utf8');
// 自分の得意な言語で
// Let's チャレンジ!!
var lines = [];
var reader = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
reader.on('line', (line) => {
lines.push(line);
});
reader.on('close', () => {
console.log(lines); // linesに書き換える
});
すると、提出コードのアウトプット
という項目の中に以下の配列が出力されると思います。
[ '2 3', '###', '...' ]
先ほどの入力例1という文字列が、lines
という変数名で、改行ごとに区切られた配列として与えられていることが分かったと思います。また、1つ目の要素である'2 3'
という文字列は今のところ必要ないので、パワフル職人sliceさんによって切り落としてもらう必要があります。まずは、これをやってみましょう。
コードを以下の通り書き換えて、提出前動作確認ボタンをクリックします。
process.stdin.resume();
process.stdin.setEncoding('utf8');
// 自分の得意な言語で
// Let's チャレンジ!!
var lines = [];
var reader = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
reader.on('line', (line) => {
lines.push(line);
});
reader.on('close', () => {
let lines_slice = lines.slice(1);
console.log(lines_slice);
});
[ '###', '...' ]
うまく1つめの要素だけ切り落とすことができました。ここで一旦、現状をスシでおさらいしておきましょう。記号#
記号.
の1文字ずつが、それぞれスシです。シャープを卵、ドットをサーモンとして置き換えると、以下のような状態になっています。
注目すべき点は、配列の要素がいずれも「シャリがつながったスシ」の状態になっていることです。この状態ではスシが食べづらいので、手先が器用な職人splitさんに分割してもらう必要があります。しかし、ここで注意すべき点は「複数の"シャリがつながったスシ"がスシゲタに乗っていること」です。この状態のスシに手を加えるためには、一旦回転ずしのベルトコンベアに乗せてあげる必要があります。また、ベルトコンベアに乗せた各スシに手を加える必要があるため、ここはmap関数を使用する必要があります。
つまり、mapとsplitの合わせ技が必要という事です。これを実際にコーディングすると、以下のような形になります。
process.stdin.resume();
process.stdin.setEncoding('utf8');
// 自分の得意な言語で
// Let's チャレンジ!!
var lines = [];
var reader = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
reader.on('line', (line) => {
lines.push(line);
});
reader.on('close', () => {
let lines_slice = lines.slice(1);
let lines_map = lines_slice.map(element => {
return element.split("");
});
console.log(lines_map);
});
[ [ '#', '#', '#' ], [ '.', '.', '.' ] ]
これでようやく、スシのシャリがつながった状態が解消されました。図解すると、以下のように「スシゲタの上に乗っているスシゲタ」の上に、スシが乗っている状態となります。
こうした「配列の中に配列が入っている状態」を2次元配列と呼び、この2次元配列に関する問題こそがpaizaBランク問題の真骨頂となります。2次元配列問題を制する者は、paizaBランクを制することができます。
とはいえ、配列が2重になったところでやることは単純です。ただ単に、forEachやfilter, mapといったベルトコンベア系の関数を2重にかけてあげれば、大概のBランク問題は解くことができるでしょう。
まず、1重のforEach関数を使って要素をconsole.log()
してみます。
process.stdin.resume();
process.stdin.setEncoding('utf8');
// 自分の得意な言語で
// Let's チャレンジ!!
var lines = [];
var reader = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
reader.on('line', (line) => {
lines.push(line);
});
reader.on('close', () => {
let lines_slice = lines.slice(1);
let lines_map = lines_slice.map(element => {
return element.split("");
});
lines_map.forEach(elements => {
console.log(elements);
});
});
[ '#', '#', '#' ]
[ '.', '.', '.' ]
2つある配列の要素が、2回に分けて出力されました。続いて、要素elementsをさらにforEachにかけてみます。
process.stdin.resume();
process.stdin.setEncoding('utf8');
// 自分の得意な言語で
// Let's チャレンジ!!
var lines = [];
var reader = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
reader.on('line', (line) => {
lines.push(line);
});
reader.on('close', () => {
let lines_slice = lines.slice(1);
let lines_map = lines_slice.map(element => {
return element.split("");
});
lines_map.forEach(elements => {
elements.forEach(element => {
console.log(element);
});
});
});
#
#
#
.
.
.
2つある配列の要素に含まれる3つの要素が、2*3=計6回に分けて出力されました。これで、2次元配列のすべての要素に対して処理を行うことが可能となりました。
さて、ここからが本題ですが、今回はこの状態で「線対象かどうか」を判断する方法について考えてみます。線対象には2種類あり、上下対象のものと左右対称のものがあります。これらはいずれも、上下または左右の逆側にある要素と比較して、それが同じであれば対象である、ということになります。よって、「逆側にある要素と比較する」という方法が分かれば、線対称の判定ができます。
ここで一旦、配列の基本について考えてみます。配列には複数の要素を含むことができます。スシで言うと、スシゲタには複数のスシを乗せられる、という意味です。そして、配列の各要素には番号が振られており、1番目の要素から順に0, 1, 2,...
という0から始まる連番が振られています。
2次元配列の場合も同様です。例えば、以下の図でいう所の、一番右上の卵寿司は、lines_map[0][2]
という番号で取得することができます。
また、この卵寿司の上下対象にある要素は、lines_map[1][2]
であるサーモンです。スシの内容が違うので、つまり上下対象ではないということになります。左右についても同様に見ていくと、lines_map[0][0]
に位置するスシは卵なので、これは左右対称であるということになります。
このようにすべてのスシに対して逆側のスシを比較していけばいいわけです。あとはこの「逆側」という法則さえわかればいいことになります。
逆側の法則を導き出すにあたっては、より大きい配列を使うとわかりやすいです。例えば、6x6の配列について考えてみましょう。6x6の配列というのは、6個のスシゲタの上に、それぞれ6個のスシが乗っている状態のことです。
まずは両端の要素について、配列の添え字を見てみましょう。一番左上の要素の添字は[0][0]
で、一番右上は[0][5]
です。5というのは配列の行数6から1を引いた数字です。つまり、一番右の要素は配列の行数から1を引いた数字
が添え字になっています。
続いて一つ内側に寄った要素の添え字を見ていきましょう。[0][1]
に対して左右対称になるのは[0][4]
です。つまり、左側の添え字が1増えていくたびに、右側の要素は添え字が1減っていく
ということです。
また、ここまでは一番上の行である0行目について扱いましたが、残りの1~5行目についてもまったく同じになります。
よって、ここまでの情報をまとめると、左右対称の場合は以下の法則が適用できることが分かります。
- 一番端の要素は[y][0]と[y][配列の行数-1]である(yは任意の整数)
- 左側の添え字がxずつ増えるたびに、右側の添え字はxずつ減っていく(xは任意の整数)
この2つをさらにまとめると、以下のことが言えます。
- 要素の位置を[y][x]とすると、左右対称位置の要素の添え字は[y][配列の行数-1-x]である
つまり、左右線対称となる図形というのは、以下の法則性を持つという事が分かります。
- その図形の各要素に対して、[y][x]の位置にある要素と[y][配列の行数-1-x]の位置にある要素が一致する。
あとは、これを解答欄のコードに反映すれば左右対称の判定ができます。
process.stdin.resume();
process.stdin.setEncoding('utf8');
// 自分の得意な言語で
// Let's チャレンジ!!
var lines = [];
var reader = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
reader.on('line', (line) => {
lines.push(line);
});
reader.on('close', () => {
let lines_slice = lines.slice(1);
let lines_length_y = Number(lines[0].split(" ")[0]); // 列数
let lines_length_x = Number(lines[0].split(" ")[1]); // 行数
let line_symmetry_y = true; // 上下線対称
let line_symmetry_x = true; // 左右線対称
let lines_map = lines_slice.map(element => {
return element.split("");
});
lines_map.forEach((elements, y) => {
elements.forEach((element, x) => {
if(lines_map[y][x] != lines_map[y][lines_length_x-1-x]) {
line_symmetry_x = false;
}
});
});
if(line_symmetry_x) {
console.log("line symmetry");
}
});
追加部分が多いので、1つ1つ説明していきます。
let lines_length_y = Number(lines[0].split(" ")[0]); // 列数
let lines_length_x = Number(lines[0].split(" ")[1]); // 行数
まずはこの部分です。この部分では、配列の行数と列数を取得しています。lines[0]
というのは最初のほうで切り落とした'2 3'
という文字列部分です。このうち2
が配列の列数であり、3
が配列の行数なので、まずはsplit関数によって半角スペース区切りで[2,3]
という配列にしたあと、[2,3][0]
によって2
を、[2,3][1]
によって3
を取得しています。また、これらの数字は取得直後は文字列となっているため、Number()
という関数によって数字に変換を行いました。
let line_symmetry_y = true; // 上下線対称
let line_symmetry_x = true; // 左右線対称
線対称の判定に使うフラグです。初期値をtrueとしたうえで、「もし反対側の要素が一致しなかったらfalseにする」という処理を挟み、最終的にtrueかどうかで線対称の判定を行います。
lines_map.forEach((elements, y) => {
elements.forEach((element, x) => {
if(lines_map[y][x] != lines_map[y][lines_length_x-1-x]) {
line_symmetry_x = false;
}
});
});
線対称の判定を行っている部分です。まず、forEachの内部関数の引数が2つになっていることに気が付きます。
forEach(elements => {})
forEach((elements, y) => {})
forEachの内部関数に2つ目の引数を与えると、forEachループの回数が0,1,2,...
という数字で与えられます。これをそのまま配列の添え字として使うことができるので、先ほど説明した法則をそのまま記述することができるようになります。
if(lines_map[y][x] != lines_map[y][lines_length_x-1-x]) {
line_symmetry_x = false;
}
lines_map[y][x]
というのが今確認中の要素で、lines_map[y][lines_length_x-1-x]
が反対側の要素です。2つの要素が一致しなかったら線対称ではないので、line_symmetry_x
をfalse
にします。
if(line_symmetry_x) {
console.log("line symmetry");
}
線対称であれば、line symmetry
と出力しています。あとで、上下対象の場合、点対象の場合、いずれにもあてはまらない場合の出力についても追加します。
以上で、左右対称の判定ができました。実は、ここまでの事が理解できれば、残りの「上下対象」と「点対象」はあっと言う間に解くことができます。
まず、上下対象について考えてみます。これは左右対称のときと考え方は全く一緒で、両端の要素と、それを内側に1つずらした要素を確認してみます。
非常に見覚えのある数字が並んでいますね。つまり、こういうことです。
- 要素の位置を[y][x]とすると、上下対称位置の要素の添え字は[配列の列数-1-y][x]である
点対象もまったく同様の考え方で解くことができます。
- 要素の位置を[y][x]とすると、点対称位置の要素の添え字は[配列の列数-1-y][配列の行数-1-x]である
よって、これらすべてをコードに反映すると、以下のようになります。
process.stdin.resume();
process.stdin.setEncoding('utf8');
// 自分の得意な言語で
// Let's チャレンジ!!
var lines = [];
var reader = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
reader.on('line', (line) => {
lines.push(line);
});
reader.on('close', () => {
let lines_slice = lines.slice(1);
let lines_length_y = Number(lines[0].split(" ")[0]); // 列数
let lines_length_x = Number(lines[0].split(" ")[1]); // 行数
let line_symmetry_y = true; // 上下線対称
let line_symmetry_x = true; // 左右線対称
let point_symmetry = true; // 点対象
let lines_map = lines_slice.map(element => {
return element.split("");
});
lines_map.forEach((elements, y) => {
elements.forEach((element, x) => {
if(lines_map[y][x] != lines_map[y][lines_length_x-1-x]) {
line_symmetry_x = false;
}
if(lines_map[y][x] != lines_map[lines_length_y-1-y][x]) {
line_symmetry_y = false;
}
if(lines_map[y][x] != lines_map[lines_length_y-1-y][lines_length_x-1-x]) {
point_symmetry = false;
}
});
});
let line_symmetry = line_symmetry_x || line_symmetry_y;
if(line_symmetry && point_symmetry) {
console.log("line point symmetry");
} else if(line_symmetry) {
console.log("line symmetry");
} else if(point_symmetry) {
console.log("point symmetry");
} else {
console.log("none");
}
});
これで、提出コードは完成です!ただ、paizaの「提出前動作確認」は1つのテストケース(今回の場合は"line symmetry"となるケース)しか判定することができません。そのため、残りの3つのケースのテストを行う方法についても説明しておきます。
テストには**paiza.IO(パイザ アイオー)**というWEBサイトを活用します。
このサイトの使い方はpaizaの解答欄とほぼ同じですが、paiza.IOでは入力値を自分で設定することができます。上記の解答欄のコードをpaiza.IOに張り付けたあと、画面下の「入力」という欄に以下3パターンの入力値を入力し、それぞれ実行してみましょう。
3 4
##..
#..#
..##
5 4
##..
##..
.##.
.##.
....
3 3
###
#.#
###
上から順に、"point symmetry"
, "none"
, "line point symmetry"
と出力できれば成功です!
最後に、paizaで実際にコードを提出してみましょう。
お疲れさまでした!
これで、ついにpaizaBランクの問題を解くことができました!
今回解いた問題はあくまで練習問題なのでpaizaのレートが上がることはありませんが、paizaBランクの中でも難易度2000弱というのは中々高いほうなので、この位の問題が解ければ十分本番の問題も解くことができると思います。
また、Bランク問題は2次元配列を使った問題が多いので、今回使ったforEachの2重ループなどが使えるようになれば、ほとんどの問題は解けるようになるはずです。この記事で学んだことを活かして、是非paizaBランクの獲得を目指してみてください!