diff --git a/0253_Meeting_Rooms_II/solution_ja.md b/0253_Meeting_Rooms_II/solution_ja.md index 374e0e1..7b1ba69 100644 --- a/0253_Meeting_Rooms_II/solution_ja.md +++ b/0253_Meeting_Rooms_II/solution_ja.md @@ -1,33 +1,277 @@ ## 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 + 他の方が描いたコードを見て、参考にしてコードを書き直してみる。 参考にしたコードのリンクは貼っておく。 読みやすいことを意識する。 他の解法も考えみる。 +- ## https://github.com/olsen-blue/Arai60/pull/57/files + +### 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) + +- 会議を開始時刻でソート +- 最小ヒープを使って**現在進行中の**会議の終了時刻を追跡 +- 新しい会議が始まる時、ヒープから終了済会議を取り除く +- ヒープのサイズが必要な会議室の数になる +- 参考 + - https://github.com/Ryotaro25/leetcode_first60/pull/61/files#diff-92e9dbf517861f420e88aa4cedcec79ceabc8fc133bc619d972cf04f4d3fc280R1 + - わかりやすい。Heap はいろんなところで見かけるので慣れておきたい + +```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) + +- https://github.com/olsen-blue/Arai60/pull/57/files#diff-a0ae933995d3a32d66b233c1e96d7f1bbe7ff33e80eb0997d04a4806ba5d2be5R112-R135 + + - こちらを参考に作成した + - 考え方は分かりやすくて好みだが、Java だと行数が増えるため実践では選びづらい + +- スイープラインとは + + - 特定の「線」(または点)を、ある空間(たとえば平面や時間軸)上で一定方向に動かしながら、途中で発生する「イベント」を順次処理していくアルゴリズム設計の手法をこう呼ぶらしい + - この問題で初めて知った + - https://en.wikipedia.org/wiki/Sweep_line_algorithm#:~:text=In%20computational%20geometry%2C%20a%20sweep,critical%20techniques%20in%20computational%20geometry. + +- イベント変換: 各会議の開始時刻と終了時刻をそれぞれイベントとして配列に変換する + - 開始イベント: +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; + } +} +``` + +### Approach 4. スイープライン+累積和(座標圧縮 + 差分配列版) + +- Approach 3 の亜種 +- 252. Meeting Rooms の Approach 4 とほぼ同一の手法。返り値が違うだけ + - https://github.com/katsukii/leetcode/pull/20 + +```java +public class Solution { + public int minMeetingRooms(int[][] intervals) { + // 1. Collect all times + List times = new ArrayList<>(); + for (int[] interval : intervals) { + times.add(interval[0]); // Start + times.add(interval[1]); // End + } + + // 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); + } + + // 4. Difference array + int[] diff = new int[sortedTimes.size() + 1]; + + // 5. Set +1 / -1 + for (int[] interval : intervals) { + int start = compressedTimes.get(interval[0]); + int end = compressedTimes.get(interval[1]); + diff[start] += 1; + diff[end] -= 1; + } + + // 6. Check prefix sum, which means ongoing meetings. + int roomsInUse = 0; + int maxRooms = 0; + for (int i = 0; i < sortedTimes.size(); i++) { + roomsInUse += diff[i]; + maxRooms = Math.max(maxRooms, roomsInUse); + } + return maxRooms; + } +} ``` ## Step 3 + 今度は、時間を測りながら、もう一回書く。 -アクセプトされたら消すを3回連続できたら問題はOK。 +アクセプトされたら消すを 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(); + } +} ```