Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
342 changes: 342 additions & 0 deletions arai60/Dynamic_Programming/unique-paths-ii.md
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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここで、

if(this.obstacleGrid[y][x] == 1){
            return;
        }

とすれば、L27~29と、L45, L49の後半の条件がいらなくなりますね。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nittoco
返信が大変遅くなりました。

たしかにそうですね。Step 4 に修正版を加えました: 83d0360

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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 がマジックナンバーになってるので、名前をつけてあげてほしいです

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The 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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

個人的にここのコードは読みにくかったです。
このロジックで実装するなら、L116~L131までのどこかで関数化した方が読みやすいかもしれません。

Copy link
Copy Markdown
Owner Author

@seal-azarashi seal-azarashi Oct 28, 2024

Choose a reason for hiding this comment

The 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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

細かくて趣味の範囲そうですが、

uniquePathCache[0] = 1;
if (leftColumnFirstObstacleIndex <= y) {
                uniquePathCache[0] = 0;
} 

と自分ならするかも(障害があるより向こう側は0になるというのが特別な感じがするので)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The 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];
Copy link
Copy Markdown

@Yoshiki-Iwasa Yoshiki-Iwasa Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このuniquePathCacheという配列についてコメントで説明がほしいです

uniquePathCache[i] が ある列におけるi行目に到達する方法の数だというのは名前から自明でなく、コードを読んでいかないとわからないので

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The 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];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

キャッシュはそのkeyに対応する値が存在すれば値を算出するための計算をスキップしてその値を返すものとして使われるイメージがあり、この変数にキャッシュと名付けるのは自分には少し違和感がありました。
https://en.wikipedia.org/wiki/Cache_(computing)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fhiyo
返信が大変遅くなりました。

この変数は命名するのに悩みました。確かに各イテレーションでは計算のスキップは行われないので、cache が名前に含まれている違和感があるのは理解できます。
少し考えてみたのですが、uniquePathCounts とするのはいかがでしょうか?これでしたら key に対応する値がユニークな path の数である以外の意味は含まれないので、上記のような違和感はないかと思います。

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];
}
}
```