Spiral Matrix
m 行 n 列の整数行列が、行のリストとして与えられます。すべての値をらせん状の順序で返してください。
左上隅から始めて、最上段を右へ進み、次に右端の列を下へ、最下段を左へ、左端の列を上へ進みます。すべての値をちょうど一度ずつ読み取るまで、時計回りに内側へ進み続けてください。
関数
- matrixinteger-2d-array
- 同じ長さの行のリストとして表した整数のグリッド
- 戻り値integer-array
- 左上隅から始めて、行列のすべての値を時計回りの螺旋順に
制約
1 ≤ m, n ≤ 80。ここで、m = matrix.length、n = matrix[i].length- 各行の長さはすべて
nです。 -100 ≤ matrix[i][j] ≤ 100
例
- 入力
- matrix = [[1, 2, 3], [10, 11, 4], [9, 12, 5], [8, 7, 6]]
- 出力
- [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
- 説明
- 値はらせん状に数え上がっていきます。外側のリングは、上辺に沿って
1, 2, 3、右側を下って4, 5, 6、下辺に沿って戻りながら7, 8、左側を上って9, 10と読みます。内側の層は1列だけで、上から下へ1回だけ読みます:11, 12。
- 入力
- matrix = [[7, 1, 5, 3], [2, 9, -4, 6], [8, 0, 4, -1]]
- 出力
- [7, 1, 5, 3, 6, -1, 4, 0, 8, 2, 9, -4]
- 説明
- 外側のリングは
7, 1, 5, 3となり、続いて右側を下へ6, -1、下辺に沿って戻るように4, 0, 8、そして左側を上へ2となります。残るのは1行だけの9, -4で、左から右へ1回読みます。
- 入力
- matrix = [[4], [1], [7]]
- 出力
- [4, 1, 7]
- 説明
- 1 列は上から下へ読み取ります。すべての値はすでに読み取られているため、上に戻る方法はありません。
提出時に隠しテスト+15件
発展問題
代わりに、左上の角から始めて、まず左側の列を下へ進む反時計回りの順序で値を返せますか?
ヒント
1つずつ開いてください。開くたびに少しずつ答えに近づきます。
1周分で何が読み取れるか見てみましょう。上の行、右の列、下の行、左の列です。その1周の後、行列には何が残っていますか?
1周すると、残りはより小さな行列になります。上端と下端ではそれぞれ1行短くなり、左右ではそれぞれ1列狭くなります。
top、bottom、left、rightの4つの境界を保持し、周回するたびに内側へ移動させます。最後の層に注意してください。1行または1列だけの場合があります。top ≤ bottomかつleft ≤ rightである間、上の行をleftからrightまで読み取り、次に右の列をtop+1からbottomまで読み取ります。top < bottomかつleft < rightの場合に限り、下の行をright-1からleftへ戻るように読み取り、左の列をbottom-1からtop+1へ上がるように読み取ります。その後、4つの境界をすべて1ステップ内側へ移動します。
解説
ここに巧妙な数学はありません。問題は要素の管理であり、解法が破綻するのはその管理の部分です。各隅は二度ではなく一度だけ読み取る必要があります。また、最内層は1行だけ、または1列だけの場合があり、その場合は一周すると同じ値を再びたどることになります。進行を妨げられるたびに右に曲がり、読み取ったセルを記憶するロボットのように進む方法があります。あるいは、4つの境界を縮めながら行列を一重ずつ剥がしていく方法もあり、こちらは追加のメモリを必要としません。
歩き、行き止まりになったら右に曲がる
考え方
右を向いた歩行者が左上のセルにいるところを想像してください。歩行者は立っているセルを読み取り、それから前に進もうとします。その一歩で行列の外に出るか、すでに読み取ったセルに着く場合は、右に曲がり(右、下、左、上、そして再び右)、代わりにその方向へ進みます。このルールによってらせんが描かれます。行列の端が最初の周回を止め、その後の周回では、それまでに読み取ったセルが壁の役割を果たします。
方向は、2つの小さな配列 dr = [0, 1, 0, -1] と dc = [1, 0, -1, 0] のインデックス d として保持します。これにより、右折は d = (d+1) % 4 となります。行列と同じサイズのブール値グリッド seen を用意します。最初の例では、歩行者は 1, 2, 3 を読み取り、右端に来ると下に曲がって 4, 5, 6 を読み取り、左に曲がって 7, 8 を読み取り、上に曲がって 9, 10 を読み取ります。10 の上には、すでに読み取った 1 があるため、右に曲がって 11 に進みます。11 の右には、すでに読み取った 4 があるため、下に曲がって 12 に進みます。
ループをちょうど m × n 回、つまりセルごとに1回実行すれば、終点を検出する必要はありません。最後に読み取った後、歩行者は壁の方を向いているかもしれませんが、もう一度進むことはありません。各セルは1回ずつ読み取られるため、時間計算量は O(m × n) です。seen グリッドには追加で O(m × n) のメモリが必要ですが、次の方法ではこれを取り除きます。
アルゴリズム
- 行
0、列0から始め、右を向き、すべてが false のseenグリッドを用意します。 m × n回繰り返します。現在の値を追加し、そのセルを訪問済みにします。- 現在の方向に進んだ次のセルを計算します。行列の外にあるか、すでに訪問済みの場合は、右に曲がって再度計算します。
- そのセルに移動します。
- 追加した順に値を返します。
def spiralOrder(matrix):
rows, cols = len(matrix), len(matrix[0])
seen = [[False] * cols for _ in range(rows)]
# Directions in clockwise order: right, down, left, up.
dr = [0, 1, 0, -1]
dc = [1, 0, -1, 0]
r = c = d = 0
result = []
for _ in range(rows * cols):
result.append(matrix[r][c])
seen[r][c] = True
nr, nc = r + dr[d], c + dc[d]
# Blocked by the edge or by a cell already read: turn right.
if not (0 <= nr < rows and 0 <= nc < cols) or seen[nr][nc]:
d = (d + 1) % 4
nr, nc = r + dr[d], c + dc[d]
r, c = nr, nc
return result4つの境界で層を剥がす
考え方
螺旋は、入れ子状になったリングの集合です。現在のリングを4つの境界で表します。行はtopからbottomまで、列はleftからrightまでです。1周では、上端の行をleftからrightまで読み、右端の列をtop+1からbottomまで下に読み、下端の行をright-1からleftまで戻って読み、左端の列をbottom-1からtop+1まで上に読みます。各辺は、直前の辺の終点から1セル先から始まるため、各角はちょうど1回だけ読み取られます。次に、4つの境界をすべて1つ内側に進め、top ≤ bottomかつleft ≤ rightである間、繰り返します。
注意すべきなのは、リングが1行または1列だけの厚さしかない場合、戻る経路がすでに読み取ったセルを通ることです。2つ目の例では、外側のリングを読み終えると、境界はtop = bottom = 1、left = 1、right = 2になります。これは1行だけの9, -4です。上端の行で両方の値を読み取り、右端の列にはtopより下のセルがありません。しかし、下端の行は上端と同じ行なので、逆向きにたどると9を2回目に加算してしまいます。したがって、下端の行と左端の列をたどるのは、top < bottomかつleft < rightの場合だけにします。3つ目の例はその逆のケースです。1列だけの4, 1, 7では、左端の列を上に戻ると1を再び読み取ってしまいます。
各値は1回だけ読み取られるため、実行時間はO(m × n)です。答えにすべての値を格納するので、これは可能な限り小さい計算量です。答え以外に必要なメモリは整数4個分です。
アルゴリズム
top = 0、bottom = m-1、left = 0、right = n-1を設定します。top ≤ bottomかつleft ≤ rightの間、上の行をleftからrightまで読み取り、右の列をtop+1からbottomまで読み取ります。top < bottomかつleft < rightの場合、下の行をright-1からleftまで読み取り、左の列をbottom-1からtop+1まで読み取ります。topとleftに1を加え、bottomとrightから1を引きます。- 読み取った順に値を返します。
def spiralOrder(matrix):
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1
result = []
while top <= bottom and left <= right:
# Top row, left to right, then right column, top to bottom.
for c in range(left, right + 1):
result.append(matrix[top][c])
for r in range(top + 1, bottom + 1):
result.append(matrix[r][right])
# A layer one row or one column thick has no way back:
# walking back would read the same cells again.
if top < bottom and left < right:
# Bottom row, right to left, then left column, bottom to top.
for c in range(right - 1, left - 1, -1):
result.append(matrix[bottom][c])
for r in range(bottom - 1, top, -1):
result.append(matrix[r][left])
# Step in to the next layer.
top += 1
bottom -= 1
left += 1
right -= 1
return result
落とし穴と境界ケース
ループは短いため、バグは角や最後の層に潜んでいます。
- 最後の層が1行または1列のときに、2回読み取ってしまう。
top < bottomとleft < rightのチェックがないと、2つ目の例では9, -4, 9となり、3つ目の例では4, 1, 7, 1と読み取られます。 - 角を2回読み取ってしまう。それぞれの辺を、その辺の最初のセルから最後のセルまで処理すると、各角は2つの辺によって読み取られます。各辺の開始位置は、前の辺の終了位置の1つ先にします。
top ≤ bottomではなくtop < bottomの間ループする。奇数サイズの正方形では中央に到達する前に止まり、3 × 3行列の中央の値が読み取られません。- 正方形でない行列で、行と列を取り違える。両方の境界に
matrix.lengthを使うと、すべての正方形のテストでは動作しますが、3 × 4では失敗します。 - 薄い入力、つまり1行、1列、1セルの場合を忘れる。それぞれ単一の層であり、最下行や左端の列には到達しません。
- Rでは、
a > bのときa:bは降順になるため、3:2のような空の範囲は何も返さずに3, 2を返します。条件で防ぐか、seq_lenを使ってください。LuaとRでは、行と列のインデックスは1から始まります。
よくある質問4
Spiral Matrix の時間計算量と空間計算量は何ですか?
どちらの方法も各値を一度ずつ読み取るため、実行時間は O(m × n) です。答えにはすべての値が含まれるため、これより効率のよい解法はありません。4つの境界を使って層を剥がす方法では、答え以外に O(1) の追加メモリを使用します。進めなくなったら方向転換する方法では、読み取ったセルを記録するために O(m × n) のグリッドを使用します。
スパイラル走査で値を2回読み取らないようにするには、どうすればよいですか?
重複が起こる箇所は2か所あります。角では、各辺の開始位置を前の辺の終点から1セル先にすることで、各角が1つの辺だけに属するようにします。最後の層では、層に行も列も2つ以上ある場合に限り、最下行と左端の列を読み取ります。そうしないと、戻るときにすでに読み取ったセルを通ることになるためです。
行列を読み取るのではなく、らせん状に値を埋めるにはどうすればよいですか?
同じ4つの境界と同じ4辺を使いますが、読み取るのではなく書き込みます。1から始まるカウンターを用意し、通過する各セルにその値を格納して、通過するたびに1ずつ増やします。n × n行列では、カウンターはn²で終わります。上記の最初の例は、4 × 3のグリッドに対してこの処理を行った結果です。
道がふさがれているときに右に曲がると、なぜ渦巻きになるのでしょうか?
最初の周回では、歩行者は行列の四隅で向きを変えます。それ以降の周回では、読み取ったセルが壁として機能するため、毎回、前回歩いた輪より1セル手前で向きを変えます。こうして各周回は前の周回の内側に収まり、らせん状になります。歩行者は自分がどの層にいるかを知る必要はなく、次のセルが空いているかどうかだけを確認すればよいのです。
似た問題
同じ考え方を使う問題です。2〜3問解くとパターンが身につきます。
Python
def spiralOrder(matrix):
# ここにコードを書いてくださいケース1
ケース2
ケース3
入力
matrix = [[1, 2, 3], [10, 11, 4], [9, 12, 5], [8, 7, 6]]
期待値
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]