今回は paiza の「落ちものシミュレーション」の問題に挑戦!
🧩 問題概要
何をする問題か
- 縦
H× 横Wのフィールド上で - 長方形ブロックが上から順番に落ちてくる
- 各ブロックは
- 下に他のブロック または
- フィールドの底
に接触した瞬間に その位置で固定
入力
フィールド情報
-
H:縦のサイズ(行数) -
W:横のサイズ(列数) -
N:落ちてくるブロックの数
各ブロックの情報
-
h_i:ブロックの高さ -
w_i:ブロックの幅 -
x_i:左端の位置(0-index)
※ すべて フィールド内に収まることは保証されている
H W N
h_1 w_1 x_1
h_2 w_2 x_2
...
h_N w_N x_N
ブロックの挙動
- ブロックは 回転しない
- 横位置
x_iは固定 - 上から落ちてきて、これ以上 下に行けなくなった行で止まる
- 止まったら、その位置に
#でブロックを描画
出力
- 最終的なフィールドを 上から順に
.(空)と#(ブロック)で出力 - 行数は
H - 各行の文字数は
W
入力例:
7 10 4
1 8 1
4 1 5
1 6 2
2 2 0
出力例:
..........
..######..
.....#....
.....#....
##...#....
##...#....
.########.
✅OK例:
// 標準入力を受け取るための readline 設定
const rl = require('readline').createInterface({ input: process.stdin });
const lines = [];
// 入力を1行ずつ配列に格納
rl.on('line', line => lines.push(line));
rl.on('close', () => {
// フィールドの高さ H、幅 W、ブロック数 N
const [H, W, N] = lines[0].split(' ').map(Number);
// 各ブロックの情報 [高さ, 幅, 左端の列]
const blocks = lines.slice(1).map(b => b.split(' ').map(Number));
// フィールドを '.' で初期化
const field = Array.from({ length: H }, () => Array(W).fill('.'));
// 指定された位置にブロックを配置する関数
function put(h, w, x, bottom) {
// bottom 行を底として、上に h 行分 '#'' を埋める
for (let i = bottom - h + 1; i <= bottom; i++) {
for (let j = x; j < x + w; j++) {
field[i][j] = '#';
}
}
}
// ブロックを1つずつ落とす
for (const b of blocks) {
const [h, w, x] = b;
// 上から順に落下位置を探す
LABEL:
for (let i = 0; i < H; i++) {
// ブロックの幅分、下に置けるか確認
for (let j = x; j < x + w; j++) {
// 底に到達、または下にブロックがあれば確定
if (i + 1 >= H || field[i+1][j] === '#') {
put(h, w, x, i);
break LABEL; // このブロックの処理を終了
}
}
}
}
// 完成したフィールドを出力
field.forEach(f => console.log(f.join('')));
});
🔍コードの流れ
- 標準入力からすべての行を
lines配列に格納 - 1行目から
- フィールドの高さ
H - 幅
W - ブロック数
N
を取得
- フィールドの高さ
- 2行目以降をブロック情報
[h, w, x]の配列として取得
フィールド初期化
- 高さ
H、幅Wの2次元配列fieldを作成 - すべてのマスを
'.'(空)で初期化
put 関数
- 引数:
-
h:ブロックの高さ -
w:ブロックの幅 -
x:左端の列 -
bottom:ブロックの底になる行
-
-
bottom - h + 1〜bottomの範囲に、幅w分'#'を埋める
ブロックを1つずつ落とす処理
- 各ブロック
[h, w, x]について以下を行う
落下位置の探索
- 上から下へ
i = 0→H-1まで調べる - ブロック幅分のすべての列
j = x→x + w - 1を確認
停止条件
- 次の行がフィールド外 または
- 次の行に
'#'があるマスを見つけたら- その
i行をブロックの底として確定 -
put()でブロックを配置 -
break LABELで探索終了
- その
出力
- フィールドを上から順に文字列として出力
💡 補足:短絡評価
i + 1 >= H を先に書いて範囲外アクセスを回避する!
↓初めにこう書いてエラーが出た
if (field[i+1][j] === '#' || i + 1 >= H)
↓ 修正例:
i + 1 >= H || field[i+1][j] === '#'
||(短絡評価) は左が true になった瞬間、右を一切評価しないから、範囲外アクセスを先に書くと、エラーが出ず意図したとおりに動く。
📝まとめ
① シミュレーション問題の基本構造
- 状態(
field)を二次元配列で持つ - 入力を 順番通りに処理
- 1つ処理するごとに状態を更新
② 「ブロック単位」で考える
- マス単位で処理しない
- ブロック全体が置けるかを判定
- 置く処理は 1回だけ
③ 二重ループの役割分担
- 外側:高さ方向(落下)
- 内側:幅方向(衝突チェック)
④ 境界条件の扱い
- 配列アクセス前に 必ず範囲チェック
-
i + 1 >= Hを先に書く
i + 1 >= H || field[i+1][j] === '#'
⑤ 短絡評価
-
||は左がtrueなら右を評価しない - 危険な配列アクセスは 右側
⑥ 「止まる瞬間」を正しく捉える
- 「置けるか?」ではなく
- 「次に進めなくなる瞬間」を検出
⑦ ラベル / break の使いどころ
- 二重ループを一気に抜けたい場面