From b02e1f8cb054361bb66292edcab6bc406e6ccacf Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Sun, 6 Oct 2024 11:52:34 +0900 Subject: [PATCH 1/5] unique paths 2 --- arai60/Dynamic_Programming/unique-paths-ii.md | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 arai60/Dynamic_Programming/unique-paths-ii.md diff --git a/arai60/Dynamic_Programming/unique-paths-ii.md b/arai60/Dynamic_Programming/unique-paths-ii.md new file mode 100644 index 0000000..554f883 --- /dev/null +++ b/arai60/Dynamic_Programming/unique-paths-ii.md @@ -0,0 +1,261 @@ +# 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) { + 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) { + 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; + } + 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 (1 <= y) { + uniquePathCache[y][x] += uniquePathCache[y - 1][x]; + } + if (1 <= 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]; + 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 (1 <= 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; + int[] uniquePathCache = new int[columnCount]; + 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 (1 <= x) { + uniquePathCache[x] += uniquePathCache[x - 1]; + } + } + } + return uniquePathCache[columnCount - 1]; + } +} +``` From f084c505ee7d3174a0a36296bc022b24b3957c47 Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Tue, 29 Oct 2024 08:38:11 +0900 Subject: [PATCH 2/5] fix --- arai60/Dynamic_Programming/unique-paths-ii.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/arai60/Dynamic_Programming/unique-paths-ii.md b/arai60/Dynamic_Programming/unique-paths-ii.md index 554f883..ee83889 100644 --- a/arai60/Dynamic_Programming/unique-paths-ii.md +++ b/arai60/Dynamic_Programming/unique-paths-ii.md @@ -178,10 +178,10 @@ class Solution { continue; } - if (1 <= y) { + if (0 < y) { uniquePathCache[y][x] += uniquePathCache[y - 1][x]; } - if (1 <= x) { + if (0 < x) { uniquePathCache[y][x] += uniquePathCache[y][x - 1]; } } @@ -215,7 +215,7 @@ class Solution { continue; } - if (1 <= x) { + if (0 < x) { uniquePathCache[x] += uniquePathCache[x - 1]; } } @@ -250,7 +250,7 @@ class Solution { continue; } - if (1 <= x) { + if (0 < x) { uniquePathCache[x] += uniquePathCache[x - 1]; } } From 1bce1fa6f5d8b57c0fd5c14055488ab59ea0d717 Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Tue, 29 Oct 2024 08:46:28 +0900 Subject: [PATCH 3/5] fix --- arai60/Dynamic_Programming/unique-paths-ii.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arai60/Dynamic_Programming/unique-paths-ii.md b/arai60/Dynamic_Programming/unique-paths-ii.md index ee83889..200bf30 100644 --- a/arai60/Dynamic_Programming/unique-paths-ii.md +++ b/arai60/Dynamic_Programming/unique-paths-ii.md @@ -241,6 +241,8 @@ class Solution { public int uniquePathsWithObstacles(int[][] obstacleGrid) { int rowCount = obstacleGrid.length, columnCount = obstacleGrid[0].length; + + // スタート地点から [i] に到達するまでのステップ数をキャッシュ int[] uniquePathCache = new int[columnCount]; uniquePathCache[0] = 1; for (int y = 0; y < rowCount; y++) { From 83d03607a359a233301d340c3c685acb0aa8e983 Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Tue, 29 Oct 2024 08:50:17 +0900 Subject: [PATCH 4/5] fix --- arai60/Dynamic_Programming/unique-paths-ii.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/arai60/Dynamic_Programming/unique-paths-ii.md b/arai60/Dynamic_Programming/unique-paths-ii.md index 200bf30..d11f92d 100644 --- a/arai60/Dynamic_Programming/unique-paths-ii.md +++ b/arai60/Dynamic_Programming/unique-paths-ii.md @@ -261,3 +261,44 @@ class Solution { } } ``` + +## 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); + } + } +} +``` From 08dd49f866a3254388a10199f7829aaad60f83c1 Mon Sep 17 00:00:00 2001 From: seal_azarashi Date: Wed, 30 Oct 2024 07:37:49 +0900 Subject: [PATCH 5/5] add step4 --- arai60/Dynamic_Programming/unique-paths-ii.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/arai60/Dynamic_Programming/unique-paths-ii.md b/arai60/Dynamic_Programming/unique-paths-ii.md index d11f92d..eca2139 100644 --- a/arai60/Dynamic_Programming/unique-paths-ii.md +++ b/arai60/Dynamic_Programming/unique-paths-ii.md @@ -302,3 +302,41 @@ class Solution { } } ``` + +### 一次元配列を用いたキャッシュ + +[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]; + } +} +```