From bf75a7d6f95b3947411de120c7cd9e16b70cabd2 Mon Sep 17 00:00:00 2001 From: Yusuke Katsuki Date: Tue, 15 Apr 2025 16:11:32 -0400 Subject: [PATCH 1/3] Step 1 & Step 2 --- 0253_Meeting_Rooms_II/solution_ja.md | 173 ++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 5 deletions(-) diff --git a/0253_Meeting_Rooms_II/solution_ja.md b/0253_Meeting_Rooms_II/solution_ja.md index 374e0e1..fe9a683 100644 --- a/0253_Meeting_Rooms_II/solution_ja.md +++ b/0253_Meeting_Rooms_II/solution_ja.md @@ -1,32 +1,195 @@ ## Problem -// The URL of the problem + +https://leetcode.com/problems/meeting-rooms-ii/ ## Step 1 -5分程度答えを見ずに考えて、手が止まるまでやってみる。 + +5 分程度答えを見ずに考えて、手が止まるまでやってみる。 何も思いつかなければ、答えを見て解く。ただし、コードを書くときは答えを見ないこと。 動かないコードも記録する。 -正解したら一旦OK。思考過程もメモする。 +正解したら一旦 OK。思考過程もメモする。 ### Approach -* + +- 頭の中でなんとなく直感が働き starts と ends に分けて 2 つに分けてソートするところまでは自力でいったが、for 文の中身が思いつかず +- 答えを見た後に時系列で図解したら理解できた +- ロジックの詳細は Step2 の Approach 1 に記載 ```java +class Solution { + public int minMeetingRooms(int[][] intervals) { + if (intervals == null || intervals.length == 0) { + return 0; + } + + int[] starts = new int[intervals.length]; + int[] ends = new int[intervals.length]; + + for (int i = 0; i < intervals.length; i++) { + starts[i] = intervals[i][0]; + ends[i] = intervals[i][1]; + } + Arrays.sort(starts); + Arrays.sort(ends); + int count = 0; + int endIndex = 0; + for (int i = 0; i < intervals.length; i++) { + if (starts[i] < ends[endIndex]) { + count++; + continue; + } + endIndex++; + } + + return count; + } +} ``` ## Step 2 + 他の方が描いたコードを見て、参考にしてコードを書き直してみる。 参考にしたコードのリンクは貼っておく。 読みやすいことを意識する。 他の解法も考えみる。 +### Approach 1. 開始・終了時刻それぞれのソート済配列を使用 + +時間計算量: O(n log n) ※ 配列のソート +空間計算量: O(n) + +- すべての会議の開始時刻を配列 starts に格納し、昇順でソート。終了時刻も同様に配列 ends に格納し、昇順でソート +- 開始時刻配列をループで走査。新しい会議が始まる前に終わる会議があれば、その会議室を再利用。そうでなければ、新しい会議室が必要 +- 感想 + - Step1 ですんなりいけるかと思ったが意外と頭がこんがらがっててこずった + - 開始時刻配列と終了時刻配列それぞれをソートした時点で、それぞれのインデックスが必ずしも同一の会議を指していない事を直感で理解できていなかったことが原因 + - 時系列に図解してそれぞれの時刻にインデックスをナンバリングしたらようやく理解できた + +```java +class Solution { + public int minMeetingRooms(int[][] intervals) { + if (intervals == null || intervals.length == 0) { + return 0; + } + + // Separate intervals array into starts and ends + int[] starts = new int[intervals.length]; + int[] ends = new int[intervals.length]; + + for (int i = 0; i < intervals.length; i++) { + starts[i] = intervals[i][0]; + ends[i] = intervals[i][1]; + } + Arrays.sort(starts); + Arrays.sort(ends); + + int roomCount = 0; + int endIndex = 0; + for (int i = 0; i < intervals.length; i++) { + if (starts[i] < ends[endIndex]) { + roomCount++; + continue; + } + endIndex++; + } + + return roomCount; + } +} +``` + +### Approach 2. 最小ヒープ(PriorityQueue)を用いる方法 + +時間計算量: O(n log n) +空間計算量: O(n) + +- 会議を開始時刻でソート +- 最小ヒープを使って**現在進行中の**会議の終了時刻を追跡 +- 新しい会議が始まる時、ヒープから終了済会議を取り除く +- ヒープのサイズが必要な会議室の数になる + ```java +public class Solution { + public int minMeetingRooms(int[][] intervals) { + if (intervals == null || intervals.length == 0) { + return 0; + } + + // Sort by the start time of each meeting + Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); + + // Trace the end-time as minimum rooms + PriorityQueue minEndTimeHeap = new PriorityQueue<>(); + minEndTimeHeap.add(intervals[0][1]); // Add first mtg end-time + // Second and later meetings + for (int i = 1; i < intervals.length; i++) { + // If current mtg starts after earliest ending meeting + if (intervals[i][0] >= minEndTimeHeap.peek()) { + minEndTimeHeap.poll(); // Release the meeting room to reuse + } + // Assign the current end-time as a newly necessary meeting room + minEndTimeHeap.add(intervals[i][1]); + } + + return minEndTimeHeap.size(); + } +} +``` + +### Approach 3. スイープラインを利用する方法 + +時間計算量: O(n log n) +空間計算量: O(n) + +- イベント変換: 各会議の開始時刻と終了時刻をそれぞれイベントとして配列に変換する + - 開始イベント: +1 カウント(会議室使用開始) + - 終了イベント: -1 カウント(会議室リリース) +- イベントソート: 両イベント配列を 2D 配列として一つの配列にマージして時刻順(昇順)にソートする。同時刻にイベントが複数存在する場合、終了イベント(-1)を優先する +- スイープ処理: ソート済のイベントをループで順番に処理し、イベントごとにカウントを更新する。カウントの最大数が必要な会議室の最小数となる + +```java +public class Solution { + public int minMeetingRooms(int[][] intervals) { + if (intervals == null || intervals.length == 0) { + return 0; + } + + // Store into an array for each start and end event + int[][] events = new int[intervals.length * 2][2]; + int index = 0; + for (int[] interval : intervals) { + // Events [Start: +1], [End: -1] + events[index++] = new int[]{interval[0], 1}; // Start + events[index++] = new int[]{interval[1], -1}; // End + } + + // Sort by time (if the times are the same, the end time takes priority) + Arrays.sort(events, (a, b) -> { + if (a[0] != b[0]) { + return Integer.compare(a[0], b[0]); // by time + } + return Integer.compare(a[1], b[1]); + }); + + int roomsInUse = 0; + int maxRooms = 0; + // Process events sequentially + for (int[] event : events) { + roomsInUse += event[1]; // Start:+1, End:-1 + maxRooms = Math.max(maxRooms, roomsInUse); + } + + return maxRooms; + } +} ``` ## Step 3 + 今度は、時間を測りながら、もう一回書く。 -アクセプトされたら消すを3回連続できたら問題はOK。 +アクセプトされたら消すを 3 回連続できたら問題は OK。 ```java From d54c86be372142922801218594c25f9316414680 Mon Sep 17 00:00:00 2001 From: Yusuke Katsuki Date: Thu, 17 Apr 2025 15:23:26 -0400 Subject: [PATCH 2/3] Comment --- 0253_Meeting_Rooms_II/solution_ja.md | 52 +++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/0253_Meeting_Rooms_II/solution_ja.md b/0253_Meeting_Rooms_II/solution_ja.md index fe9a683..6c48d7a 100644 --- a/0253_Meeting_Rooms_II/solution_ja.md +++ b/0253_Meeting_Rooms_II/solution_ja.md @@ -54,6 +54,9 @@ class Solution { 読みやすいことを意識する。 他の解法も考えみる。 +- まず ChatGPT で典型的な解法を調べてみて、Approach 1〜3 を確認。その上で PR をみていく +- https://github.com/olsen-blue/Arai60/pull/57/files + ### Approach 1. 開始・終了時刻それぞれのソート済配列を使用 時間計算量: O(n log n) ※ 配列のソート @@ -138,11 +141,17 @@ public class Solution { } ``` -### Approach 3. スイープラインを利用する方法 +### Approach 3. スイープライン+イベントベースの累積和方式 時間計算量: O(n log n) 空間計算量: O(n) +- スイープラインとは + + - 特定の「線」(または点)を、ある空間(たとえば平面や時間軸)上で一定方向に動かしながら、途中で発生する「イベント」を順次処理していくアルゴリズム設計の手法をこう呼ぶらしい + - この問題で初めて知った + - https://en.wikipedia.org/wiki/Sweep_line_algorithm#:~:text=In%20computational%20geometry%2C%20a%20sweep,critical%20techniques%20in%20computational%20geometry. + - イベント変換: 各会議の開始時刻と終了時刻をそれぞれイベントとして配列に変換する - 開始イベント: +1 カウント(会議室使用開始) - 終了イベント: -1 カウント(会議室リリース) @@ -186,6 +195,47 @@ public class Solution { } ``` +### Approach 4. スイープライン+座標圧縮 + +- + +```java + if (intervals == null || intervals.length == 0) { + return 0; + } + + // 1. すべての時間点(開始と終了)を収集 + Set timePoints = new TreeSet<>(); // TreeSetを使うと自動的にソートされる + for (int[] interval : intervals) { + timePoints.add(interval[0]); // 開始時間 + timePoints.add(interval[1]); // 終了時間 + } + + // 2. 時間点を配列に変換してインデックスにマッピング + Integer[] sortedTimes = timePoints.toArray(new Integer[0]); + Map timeToIndex = new HashMap<>(); + for (int i = 0; i < sortedTimes.length; i++) { + timeToIndex.put(sortedTimes[i], i); + } + + // 3. 圧縮された座標系でのイベント配列を作成 + int[] meetings = new int[sortedTimes.length]; + for (int[] interval : intervals) { + meetings[timeToIndex.get(interval[0])]++; // 開始時間: +1 + meetings[timeToIndex.get(interval[1])]--; // 終了時間: -1 + } + + // 4. 累積和を計算して最大値を見つける + int currentRooms = 0; + int maxRooms = 0; + for (int count : meetings) { + currentRooms += count; + maxRooms = Math.max(maxRooms, currentRooms); + } + + return maxRooms; +``` + ## Step 3 今度は、時間を測りながら、もう一回書く。 From 7349558794af577c20283c8c25f4f2711e66424e Mon Sep 17 00:00:00 2001 From: Yusuke Katsuki Date: Sun, 20 Apr 2025 16:21:52 -0400 Subject: [PATCH 3/3] Step 3 --- 0253_Meeting_Rooms_II/solution_ja.md | 87 +++++++++++++++++++--------- 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/0253_Meeting_Rooms_II/solution_ja.md b/0253_Meeting_Rooms_II/solution_ja.md index 6c48d7a..7b1ba69 100644 --- a/0253_Meeting_Rooms_II/solution_ja.md +++ b/0253_Meeting_Rooms_II/solution_ja.md @@ -54,8 +54,7 @@ class Solution { 読みやすいことを意識する。 他の解法も考えみる。 -- まず ChatGPT で典型的な解法を調べてみて、Approach 1〜3 を確認。その上で PR をみていく -- https://github.com/olsen-blue/Arai60/pull/57/files +- ## https://github.com/olsen-blue/Arai60/pull/57/files ### Approach 1. 開始・終了時刻それぞれのソート済配列を使用 @@ -111,6 +110,9 @@ class Solution { - 最小ヒープを使って**現在進行中の**会議の終了時刻を追跡 - 新しい会議が始まる時、ヒープから終了済会議を取り除く - ヒープのサイズが必要な会議室の数になる +- 参考 + - https://github.com/Ryotaro25/leetcode_first60/pull/61/files#diff-92e9dbf517861f420e88aa4cedcec79ceabc8fc133bc619d972cf04f4d3fc280R1 + - わかりやすい。Heap はいろんなところで見かけるので慣れておきたい ```java public class Solution { @@ -141,11 +143,16 @@ public class Solution { } ``` -### Approach 3. スイープライン+イベントベースの累積和方式 +### Approach 3. スイープライン+累積和(イベントソート版) 時間計算量: O(n log n) 空間計算量: O(n) +- https://github.com/olsen-blue/Arai60/pull/57/files#diff-a0ae933995d3a32d66b233c1e96d7f1bbe7ff33e80eb0997d04a4806ba5d2be5R112-R135 + + - こちらを参考に作成した + - 考え方は分かりやすくて好みだが、Java だと行数が増えるため実践では選びづらい + - スイープラインとは - 特定の「線」(または点)を、ある空間(たとえば平面や時間軸)上で一定方向に動かしながら、途中で発生する「イベント」を順次処理していくアルゴリズム設計の手法をこう呼ぶらしい @@ -195,45 +202,53 @@ public class Solution { } ``` -### Approach 4. スイープライン+座標圧縮 +### Approach 4. スイープライン+累積和(座標圧縮 + 差分配列版) -- +- Approach 3 の亜種 +- 252. Meeting Rooms の Approach 4 とほぼ同一の手法。返り値が違うだけ + - https://github.com/katsukii/leetcode/pull/20 ```java - if (intervals == null || intervals.length == 0) { - return 0; - } - - // 1. すべての時間点(開始と終了)を収集 - Set timePoints = new TreeSet<>(); // TreeSetを使うと自動的にソートされる +public class Solution { + public int minMeetingRooms(int[][] intervals) { + // 1. Collect all times + List times = new ArrayList<>(); for (int[] interval : intervals) { - timePoints.add(interval[0]); // 開始時間 - timePoints.add(interval[1]); // 終了時間 + times.add(interval[0]); // Start + times.add(interval[1]); // End } - // 2. 時間点を配列に変換してインデックスにマッピング - Integer[] sortedTimes = timePoints.toArray(new Integer[0]); - Map timeToIndex = new HashMap<>(); - for (int i = 0; i < sortedTimes.length; i++) { - timeToIndex.put(sortedTimes[i], i); + // 2. Remove duplicates and sort + Set uniqueTimes = new TreeSet<>(times); + List sortedTimes = new ArrayList<>(uniqueTimes); + + // 3. Cordinate compression + Map compressedTimes = new HashMap<>(); + for (int i = 0; i < sortedTimes.size(); i++) { + compressedTimes.put(sortedTimes.get(i), i); } - // 3. 圧縮された座標系でのイベント配列を作成 - int[] meetings = new int[sortedTimes.length]; + // 4. Difference array + int[] diff = new int[sortedTimes.size() + 1]; + + // 5. Set +1 / -1 for (int[] interval : intervals) { - meetings[timeToIndex.get(interval[0])]++; // 開始時間: +1 - meetings[timeToIndex.get(interval[1])]--; // 終了時間: -1 + int start = compressedTimes.get(interval[0]); + int end = compressedTimes.get(interval[1]); + diff[start] += 1; + diff[end] -= 1; } - // 4. 累積和を計算して最大値を見つける - int currentRooms = 0; + // 6. Check prefix sum, which means ongoing meetings. + int roomsInUse = 0; int maxRooms = 0; - for (int count : meetings) { - currentRooms += count; - maxRooms = Math.max(maxRooms, currentRooms); + for (int i = 0; i < sortedTimes.size(); i++) { + roomsInUse += diff[i]; + maxRooms = Math.max(maxRooms, roomsInUse); } - return maxRooms; + } +} ``` ## Step 3 @@ -241,6 +256,22 @@ public class Solution { 今度は、時間を測りながら、もう一回書く。 アクセプトされたら消すを 3 回連続できたら問題は OK。 +- Approach 2 の minHeap で解いた + ```java +public class Solution { + public int minMeetingRooms(int[][] intervals) { + Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); + PriorityQueue minEndTimeHeap = new PriorityQueue<>(); + minEndTimeHeap.add(intervals[0][1]); // first mtg + for (int i = 1; i < intervals.length; i++) { + if (minEndTimeHeap.peek() <= intervals[i][0]) { + minEndTimeHeap.poll(); + } + minEndTimeHeap.add(intervals[i][1]); + } + return minEndTimeHeap.size(); + } +} ```