ヒープで 水位の 上がる 池を 対角まで 渡れる いちばん早い 時刻を求める
高さの
海外で定番の問題リスト 4 本には入っていません
問題
n 行 n 列のgrid で
泳ぐ(0, 0) に(n - 1, n - 1) に
- n は 1 以上 50 以下です
- 石の
高さは 0 から n² - 1 までの 整数で、 どの 2 つも違います
入力と出力の例
例 1
左上の 0 から、
例 2
右下の
例 3
まん中の 0 の
面接での聞かれ方
面接では、
- 「グリッドの
各マスに 高さが あり、 時刻 t には 高さ t 以下の マスを 泳げます。 左上から 右下へ 着く 最小の 時刻を 返してください」 - ダイクストラ法で
書くと 「道の 長さではなく 道の いちばん高い 所を 小さく するのに、 なぜダイクストラ法が 使えるのですか?」と 聞かれます - 「時刻で
二分 探索して、 その 時刻に 着けるかを 幅優先探索で 調べる 書き方も できますか?」と 聞かれる ことがあります
泳ぎ始める
ヒント
まだ開いていません
付箋は 4 枚。下の段ほど答えに近づきます
ヒント 2
ヒント 3
ヒント 4答えに近い
アルゴリズムのチートシートの「グラフ探索と幅優先探索」に、
関わる学び
グラフ探索は、
まず 考える手順
段 1
ある
段 2
これは、
段 3
候補を(時刻, 行, 列) を
段 4
始めの(左上の石の高さ, 0, 0) です。
解答
Python
Python
from heapq import heappop
from heapq import heappush
STEPS = ((1, 0), (-1, 0),
(0, 1), (0, -1))
def swim_time(grid):
n = len(grid)
seen = {(0, 0)}
h = [(grid[0][0], 0, 0)]
last = n - 1
while True:
t, r, c = heappop(h)
if r == c == last:
return t
for dy, dx in STEPS:
y = r + dy
x = c + dx
if y < 0 or y >= n:
continue
if x < 0 or x >= n:
continue
if (y, x) in seen:
continue
seen.add((y, x))
v = grid[y][x]
v = max(v, t)
item = (v, y, x)
heappush(h, item)JavaScript
JavaScript
function swimTime(grid) {
const n = grid.length;
const seen = new Set([0]);
const h = [
[grid[0][0], 0, 0]];
while (true) {
const [t, r, c] = pop(h);
if (r === n - 1 &&
c === n - 1) {
return t;
}
for (const [y, x] of [
[r + 1, c], [r - 1, c],
[r, c + 1], [r, c - 1],
]) {
if (y < 0 || y >= n ||
x < 0 || x >= n) {
continue;
}
// マスの番号は y * n + x
const id = y * n + x;
if (seen.has(id)) {
continue;
}
seen.add(id);
const v = Math.max(
t, grid[y][x]);
push(h, [v, y, x]);
}
}
}
// [時刻, 行, 列] の小さい順のヒープ
function push(h, x) {
h.push(x);
let i = h.length - 1;
while (i > 0) {
const p = (i - 1) >> 1;
if (h[p][0] <= h[i][0]) {
return;
}
[h[p], h[i]] = [h[i], h[p]];
i = p;
}
}
function pop(h) {
const top = h[0];
const last = h.pop();
if (h.length === 0) {
return top;
}
h[0] = last;
const n = h.length;
let i = 0;
while (true) {
let s = i;
const l = 2 * i + 1;
const r = l + 1;
if (l < n &&
h[l][0] < h[s][0]) {
s = l;
}
if (r < n &&
h[r][0] < h[s][0]) {
s = r;
}
if (s === i) {
return top;
}
[h[s], h[i]] = [h[i], h[s]];
i = s;
}
}Go
Go
import "container/heap"
// {時刻, 行, 列} の小さい順のヒープ
type pq [][3]int
func (h pq) Len() int {
return len(h)
}
func (h pq) Less(
i, j int,
) bool {
return h[i][0] < h[j][0]
}
func (h pq) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
}
func (h *pq) Push(x any) {
*h = append(*h, x.([3]int))
}
func (h *pq) Pop() any {
old := *h
x := old[len(old)-1]
*h = old[:len(old)-1]
return x
}
var steps = [][2]int{
{1, 0}, {-1, 0},
{0, 1}, {0, -1},
}
func swimTime(
grid [][]int,
) int {
n := len(grid)
seen := make([][]bool, n)
for i := range seen {
seen[i] = make([]bool, n)
}
seen[0][0] = true
h := &pq{{grid[0][0], 0, 0}}
for {
top := heap.Pop(h).([3]int)
t := top[0]
r, c := top[1], top[2]
if r == n-1 && c == n-1 {
return t
}
for _, d := range steps {
y, x := r+d[0], c+d[1]
if y < 0 || y >= n ||
x < 0 || x >= n ||
seen[y][x] {
continue
}
seen[y][x] = true
v := max(t, grid[y][x])
heap.Push(h,
[3]int{v, y, x})
}
}
}この
計算量
- 時間は O(n² log n) です。
マスは n² 個で、 どれも 1 回だけヒープへ 入り、 出し入れが それぞれ O(log n²) = O(log n) です - 使う
記憶は O(n²) です。 見た 印と、 ヒープに 入る候補です
よく ある間違い
- 始めの
時刻を 0 にする。 左上の 石が 高いと、 沈む前に 泳ぎ出したことに なり、 例 3 で 3 を 返してしまいます - 時刻を
足し算で積む。 道の 長さではなく、 道の いちばん高い 石が 答えです。 足すと 例 2 で 6 になります - 幅優先探索で、
歩数の 少ない 順に 確定する。 歩数が 少なくても 高い 石を 通る 道が あり、 遠回りして 低い石だけを 通る 道を 見落とします - 右下の
マスの 高さだけを 答えにする。 例 1 では 右下の 石は 4 ですが、 途中の 5 が 沈むまで 渡れません。 例 3 のように 始めの 石の 方が 高い ときも外れます
解答を動かした版 Python 3.14.7Node.js 24.22.1Go 1.24.2確認日 2026年11月8日
面接での 話し方の例
書き始める
「着ける
時刻は、 道で 通る 石の いちばん高い 所です。 その値を 小さく する 最短経路なので、 足し算の 代わりに 大きい方を 取る ダイクストラ法を 使います。 左上を 左上の 石の 高さで 積み、 ヒープから 時刻の 小さい マスを 取り出し、 隣へ 時刻と 隣の 石の 大きい方を 積みます。 右下を 取り出したら その 時刻を 返します。 時間は O(n² log n) です。」
書き終えたら、
「例 2 で、
左上を 0 で 積みます。 取り出して、 右に 1、 下に 2 を 積みます。 1 を 取り出し、 右下に 3 を 積みます。 2 を 取り出しても 右下は 見たので 飛ばし、 3 の 右下を 取り出して 3 を 返します。」