-
Notifications
You must be signed in to change notification settings - Fork 0
63. Unique Paths II #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,342 @@ | ||
| # 63. Unique Paths II | ||
|
|
||
| LeetCode URL: https://leetcode.com/problems/unique-paths-ii/description/ | ||
|
|
||
| この問題は Java で解いています。 | ||
| 各解法において、メソッドが属するクラスとして `Solution` を定義していますが、これは Java の言語仕様に従い、コードを実行可能にするために必要なものです。このクラス自体には特定の意味はなく、単にメソッドを組織化し、実行可能にするためのものです。 | ||
|
|
||
| ## Step 1 | ||
|
|
||
| 最初に思いついた愚直な再帰の実装。 | ||
|
|
||
| ```java | ||
| /** | ||
| * かかった時間: 約12分 | ||
| * 時間計算量: O(2^(m + n)): | ||
| * - 各マスで進行方向の候補が最大で2つある (進行方向が配列の boundary を超える or obstacle があるマスは候補にならない) | ||
| * - スタートからゴールに到達するまで m - 1 + n - 1 回の移動が必要 | ||
| * 空間計算量: O(m + n): | ||
| * - スタートからゴールまでの移動回数が再帰の深さになる | ||
| * - 移動回数は m - 1 + n - 1 回 | ||
| */ | ||
| class Solution { | ||
| int[][] obstacleGrid; | ||
| int uniquePathCount; | ||
|
|
||
| public int uniquePathsWithObstacles(int[][] obstacleGrid) { | ||
| if (obstacleGrid[0][0] == 1) { | ||
| return 0; | ||
| } | ||
|
|
||
| this.obstacleGrid = obstacleGrid; | ||
| this.uniquePathCount = 0; | ||
| findPathToGoalRecursively(0, 0); | ||
| return this.uniquePathCount; | ||
| } | ||
|
|
||
| private void findPathToGoalRecursively(int y, int x) { | ||
| int rowCount = this.obstacleGrid.length; | ||
| int columnCount = this.obstacleGrid[0].length; | ||
| if (y == rowCount - 1 && x == columnCount - 1) { | ||
| this.uniquePathCount++; | ||
| } | ||
|
|
||
| int nextColumn = x + 1; | ||
| if (nextColumn < columnCount && this.obstacleGrid[y][nextColumn] == 0) { | ||
| findPathToGoalRecursively(y, nextColumn); | ||
| } | ||
| int nextRow = y + 1; | ||
| if (nextRow < rowCount && this.obstacleGrid[nextRow][x] == 0) { | ||
| findPathToGoalRecursively(nextRow, x); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| 上記書いていて自然と思いつく、二次元配列のキャッシュを使った解法。 | ||
|
|
||
| ```java | ||
| /** | ||
| * かかった時間: 約21分 | ||
| * 時間計算量: O(m * n): | ||
| * - O(m * n): 配列の宣言 | ||
| * - O(m + n): 配列の一番上の行と左端列を 1 にする (途中 obstacle がある場合は中断) | ||
| * - O(m * n): unique path の算出を各マスで実施 | ||
| * - O(1): 各イテレーションでの計算処理 | ||
| * 空間計算量: O(m * n): キャッシュ用の二次元配列 | ||
| */ | ||
| class Solution { | ||
| public int uniquePathsWithObstacles(int[][] obstacleGrid) { | ||
| int rowCount = obstacleGrid.length, columnCount = obstacleGrid[0].length; | ||
| int[][] uniquePathCache = new int[rowCount][columnCount]; | ||
| for (int i = 0; i < rowCount; i++) { | ||
| if (obstacleGrid[i][0] == 1) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1 がマジックナンバーになってるので、名前をつけてあげてほしいです
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Yoshiki-Iwasa 仰るとおりですね。step 2 以降定数として宣言するようにしました。 |
||
| break; | ||
| } | ||
| uniquePathCache[i][0] = 1; | ||
| } | ||
| for (int i = 0; i < columnCount; i++) { | ||
| if (obstacleGrid[0][i] == 1) { | ||
| break; | ||
| } | ||
| uniquePathCache[0][i] = 1; | ||
| } | ||
|
|
||
| for (int y = 1; y < rowCount; y++) { | ||
| for (int x = 1; x < columnCount; x++) { | ||
| if (obstacleGrid[y][x] == 1) { | ||
| continue; | ||
| } | ||
|
|
||
| uniquePathCache[y][x] = uniquePathCache[y - 1][x] + uniquePathCache[y][x - 1]; | ||
| } | ||
| } | ||
| return uniquePathCache[rowCount - 1][columnCount - 1]; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| さらに空間計算量を減らすため、一次元配列のキャッシュ + 左端列の最初の obstacle 出現位置を用いたキャッシュを行う。 | ||
|
|
||
| ```java | ||
| /** | ||
| * かかった時間: 不明 (ご飯食べたりシャワー浴びながら考えていた) | ||
| * 時間計算量: O(m * n): | ||
| * - O(1): 一次元配列の宣言 | ||
| * - O(n): 配列の一番上の行を 1 にする (途中 obstacle がある場合は中断) | ||
| * - O(n): 左端列をスタート地点から走査し、最初に obstacle が現れる位置を特定 | ||
| * - O(m * n): unique path の算出を各マスで実施 | ||
| * - O(1): 各イテレーションでの計算処理 | ||
| * 空間計算量: O(n): | ||
| * - O(n): キャッシュ用の一次元配列 | ||
| * - O(1): 左端列最初に obstacle が現れる位置 | ||
| */ | ||
| class Solution { | ||
| public int uniquePathsWithObstacles(int[][] obstacleGrid) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 個人的にここのコードは読みにくかったです。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nittoco 読みづらさの原因は関数が長いことでしょうか?個人的には30行程度なのであまり違和感を感じませんでしたが、例えば次のようにすれば読みやすくなるでしょうか。 class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int rowCount = obstacleGrid.length;
int columnCount = obstacleGrid[0].length;
int[] uniquePathCache = createUniquePathCache(columnCount, obstacleGrid);
int leftColumnFirstObstacleIndex = createLeftColumnFirstObstacleIndex(rowCount, obstacleGrid);
for (int y = 1; y < rowCount; y++) {
if (leftColumnFirstObstacleIndex <= y) {
uniquePathCache[0] = 0;
} else {
uniquePathCache[0] = 1;
}
for (int x = 1; x < columnCount; x++) {
if (obstacleGrid[y][x] == 1) {
uniquePathCache[x] = 0;
continue;
}
uniquePathCache[x] += uniquePathCache[x - 1];
}
}
return uniquePathCache[columnCount - 1];
}
private int[] createUniquePathCache(int columnCount, int[][] obstacleGrid) {
int[] uniquePathCache = new int[columnCount];
for (int i = 0; i < columnCount; i++) {
if (obstacleGrid[0][i] == 1) {
break;
}
uniquePathCache[i] = 1;
}
return uniquePathCache;
}
private int createLeftColumnFirstObstacleIndex(int rowCount, int[][] obstacleGrid) {
int leftColumnFirstObstacleIndex = rowCount;
for (int i = 0; i < rowCount; i++) {
if (obstacleGrid[i][0] == 1) {
leftColumnFirstObstacleIndex = i;
break;
}
}
return leftColumnFirstObstacleIndex;
}
}関数化をしたらしたで、引数に何が来るのかとかを把握しながら読むことになるので、意外とひとつにまとまっていた方が読みやすかったりしないかなとも思ったりするのですが、どうでしょうね。 |
||
| int rowCount = obstacleGrid.length; | ||
| int columnCount = obstacleGrid[0].length; | ||
| int[] uniquePathCache = new int[columnCount]; | ||
| for (int i = 0; i < columnCount; i++) { | ||
| if (obstacleGrid[0][i] == 1) { | ||
| break; | ||
| } | ||
| uniquePathCache[i] = 1; | ||
| } | ||
| int leftColumnFirstObstacleIndex = rowCount; | ||
| for (int i = 0; i < rowCount; i++) { | ||
| if (obstacleGrid[i][0] == 1) { | ||
| leftColumnFirstObstacleIndex = i; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| for (int y = 1; y < rowCount; y++) { | ||
| if (leftColumnFirstObstacleIndex <= y) { | ||
| uniquePathCache[0] = 0; | ||
| } else { | ||
| uniquePathCache[0] = 1; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 細かくて趣味の範囲そうですが、 uniquePathCache[0] = 1;
if (leftColumnFirstObstacleIndex <= y) {
uniquePathCache[0] = 0;
} と自分ならするかも(障害があるより向こう側は0になるというのが特別な感じがするので)
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. なるほど、これも良いですね。else 句は適切に使えてるのかいつも悩ましいです。 |
||
| for (int x = 1; x < columnCount; x++) { | ||
| if (obstacleGrid[y][x] == 1) { | ||
| uniquePathCache[x] = 0; | ||
| continue; | ||
| } | ||
| uniquePathCache[x] += uniquePathCache[x - 1]; | ||
| } | ||
| } | ||
| return uniquePathCache[columnCount - 1]; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Step 2 | ||
|
|
||
| ### 二次元配列を用いたキャッシュ (ループをひとつにする) | ||
|
|
||
| 他の方が書いており、こちらの方が綺麗になりそうだったので書いてみる。ついでに Obstacle を表す値 1 がマジックナンバーとなっていたので定数化。 | ||
|
|
||
| ```java | ||
| /** | ||
| * かかった時間: 約21分 | ||
| * 時間計算量: O(m * n): | ||
| * - O(m * n): 配列の宣言 | ||
| * - O(m * n): unique path の算出を各マスで実施 | ||
| * - O(1): 各イテレーションでの計算処理 | ||
| * 空間計算量: O(m * n): キャッシュ用の二次元配列 | ||
| */ | ||
| class Solution { | ||
| private static final int OBSTACLE = 1; | ||
|
|
||
| public int uniquePathsWithObstacles(int[][] obstacleGrid) { | ||
| int rowCount = obstacleGrid.length, columnCount = obstacleGrid[0].length; | ||
| int[][] uniquePathCache = new int[rowCount][columnCount]; | ||
| uniquePathCache[0][0] = 1; | ||
| for (int y = 0; y < rowCount; y++) { | ||
| for (int x = 0; x < columnCount; x++) { | ||
| if (obstacleGrid[y][x] == OBSTACLE) { | ||
| uniquePathCache[y][x] = 0; | ||
| continue; | ||
| } | ||
|
|
||
| if (0 < y) { | ||
| uniquePathCache[y][x] += uniquePathCache[y - 1][x]; | ||
| } | ||
| if (0 < x) { | ||
| uniquePathCache[y][x] += uniquePathCache[y][x - 1]; | ||
| } | ||
| } | ||
| } | ||
| return uniquePathCache[rowCount - 1][columnCount - 1]; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 一次元配列を用いたキャッシュ | ||
|
|
||
| ```java | ||
| /** | ||
| * 時間計算量: O(m * n): | ||
| * - O(1): 一次元配列の宣言 | ||
| * - O(m * n): unique path の算出を各マスで実施 | ||
| * - O(1): 各イテレーションでの計算処理 | ||
| * 空間計算量: O(n): キャッシュ用の一次元配列 | ||
| */ | ||
| class Solution { | ||
| private static final int OBSTACLE = 1; | ||
|
|
||
| public int uniquePathsWithObstacles(int[][] obstacleGrid) { | ||
| int rowCount = obstacleGrid.length, columnCount = obstacleGrid[0].length; | ||
| int[] uniquePathCache = new int[columnCount]; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. この uniquePathCache[i] が ある列におけるi行目に到達する方法の数だというのは名前から自明でなく、コードを読んでいかないとわからないので
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Yoshiki-Iwasa そうですね、確かにコメントがあった方が良さそうです。「スタート地点から [i] に到達するまでのステップ数をキャッシュ」と書こうかと思いましたが、充分な説明になってますでしょうか? |
||
| uniquePathCache[0] = 1; | ||
| for (int y = 0; y < rowCount; y++) { | ||
| for (int x = 0; x < columnCount; x++) { | ||
| if (obstacleGrid[y][x] == OBSTACLE) { | ||
| uniquePathCache[x] = 0; | ||
| continue; | ||
| } | ||
|
|
||
| if (0 < x) { | ||
| uniquePathCache[x] += uniquePathCache[x - 1]; | ||
| } | ||
| } | ||
| } | ||
| return uniquePathCache[columnCount - 1]; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Step 3 | ||
|
|
||
| ```java | ||
| /** | ||
| * かかった時間: 約3分 | ||
| * 時間計算量: O(m * n): | ||
| * - O(1): 一次元配列の宣言 | ||
| * - O(m * n): unique path の算出を各マスで実施 | ||
| * - O(1): 各イテレーションでの計算処理 | ||
| * 空間計算量: O(n): キャッシュ用の一次元配列 | ||
| */ | ||
| class Solution { | ||
| private static final int OBSTACLE = 1; | ||
|
|
||
| public int uniquePathsWithObstacles(int[][] obstacleGrid) { | ||
| int rowCount = obstacleGrid.length, columnCount = obstacleGrid[0].length; | ||
|
|
||
| // スタート地点から [i] に到達するまでのステップ数をキャッシュ | ||
| int[] uniquePathCache = new int[columnCount]; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. キャッシュはそのkeyに対応する値が存在すれば値を算出するための計算をスキップしてその値を返すものとして使われるイメージがあり、この変数にキャッシュと名付けるのは自分には少し違和感がありました。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @fhiyo この変数は命名するのに悩みました。確かに各イテレーションでは計算のスキップは行われないので、cache が名前に含まれている違和感があるのは理解できます。 |
||
| uniquePathCache[0] = 1; | ||
| for (int y = 0; y < rowCount; y++) { | ||
| for (int x = 0; x < columnCount; x++) { | ||
| if (obstacleGrid[y][x] == OBSTACLE) { | ||
| uniquePathCache[x] = 0; | ||
| continue; | ||
| } | ||
|
|
||
| if (0 < x) { | ||
| uniquePathCache[x] += uniquePathCache[x - 1]; | ||
| } | ||
| } | ||
| } | ||
| return uniquePathCache[columnCount - 1]; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Step 4 | ||
|
|
||
| ### 最初に思いついた愚直な再帰の実装 | ||
|
|
||
| [nittoco さんのレビュー](https://github.com/seal-azarashi/leetcode/pull/32#discussion_r1791907020) を受けてリファクタリングした。 | ||
|
|
||
| ```java | ||
| class Solution { | ||
| int[][] obstacleGrid; | ||
| int uniquePathCount; | ||
|
|
||
| public int uniquePathsWithObstacles(int[][] obstacleGrid) { | ||
| this.obstacleGrid = obstacleGrid; | ||
| this.uniquePathCount = 0; | ||
| findPathToGoalRecursively(0, 0); | ||
| return this.uniquePathCount; | ||
| } | ||
|
|
||
| private void findPathToGoalRecursively(int y, int x) { | ||
| if(this.obstacleGrid[y][x] == 1){ | ||
| return; | ||
| } | ||
|
|
||
| int rowCount = this.obstacleGrid.length; | ||
| int columnCount = this.obstacleGrid[0].length; | ||
| if (y == rowCount - 1 && x == columnCount - 1) { | ||
| this.uniquePathCount++; | ||
| } | ||
|
|
||
| int nextColumn = x + 1; | ||
| if (nextColumn < columnCount) { | ||
| findPathToGoalRecursively(y, nextColumn); | ||
| } | ||
| int nextRow = y + 1; | ||
| if (nextRow < rowCount) { | ||
| findPathToGoalRecursively(nextRow, x); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 一次元配列を用いたキャッシュ | ||
|
|
||
| [fhiyo さんの指摘](https://github.com/seal-azarashi/leetcode/pull/32#discussion_r1789132262)を受け、ステップ数を格納する配列の名前を修正。 | ||
|
|
||
| ```java | ||
| /** | ||
| * 時間計算量: O(m * n): | ||
| * - O(1): 一次元配列の宣言 | ||
| * - O(m * n): unique path の算出を各マスで実施 | ||
| * - O(1): 各イテレーションでの計算処理 | ||
| * 空間計算量: O(n): キャッシュ用の一次元配列 | ||
| */ | ||
| class Solution { | ||
| private static final int OBSTACLE = 1; | ||
|
|
||
| public int uniquePathsWithObstacles(int[][] obstacleGrid) { | ||
| int rowCount = obstacleGrid.length, columnCount = obstacleGrid[0].length; | ||
|
|
||
| // スタート地点から [i] に到達するまでのステップ数をキャッシュ | ||
| int[] uniquePathCounts = new int[columnCount]; | ||
| uniquePathCounts[0] = 1; | ||
| for (int y = 0; y < rowCount; y++) { | ||
| for (int x = 0; x < columnCount; x++) { | ||
| if (obstacleGrid[y][x] == OBSTACLE) { | ||
| uniquePathCounts[x] = 0; | ||
| continue; | ||
| } | ||
|
|
||
| if (0 < x) { | ||
| uniquePathCounts[x] += uniquePathCounts[x - 1]; | ||
| } | ||
| } | ||
| } | ||
| return uniquePathCounts[columnCount - 1]; | ||
| } | ||
| } | ||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ここで、
とすれば、L27~29と、L45, L49の後半の条件がいらなくなりますね。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nittoco
返信が大変遅くなりました。
たしかにそうですね。Step 4 に修正版を加えました: 83d0360