diff --git a/57_insert_interval/memo.md b/57_insert_interval/memo.md new file mode 100644 index 0000000..0f6ea77 --- /dev/null +++ b/57_insert_interval/memo.md @@ -0,0 +1,79 @@ +# 57. Insert Interval + +https://leetcode.com/problems/insert-interval/ + +## Comments + +### step1 + +* 大雑把な方針として、[interval_start, interval_end] の区間を掲げて、overlap しなくなるまで j を進める。j が進み終わったら、その時点の [interval_start, interval_end] を push_back という方針で考え始めた (この区間を extend していく感覚)。 +* `SolutionWA1` + * しかし inner while のループ条件指定でなぜかドツボにハマって 50 分くらい考え込んでしまった + * 多分 `interval_end` (今掲げている範囲)、`newInterval` (引数で与えられた範囲)、`intervals[j]` (overlap していたので進めようとしている範囲) の 3 つを同時に処理しようとして、条件設定がよくわからなくなってしまった模様。 + * 最終的に `if (new_interval_start <= interval_end)` のように `newInterval` だけ別で処理し (一回の処理なので `if` だけでよい)、inner while では `interval_end` と `intervals[j]` に注目すればよいことに気づいて腹落ちした + * しかし `[], [5, 7]` のような入力に対して落ちてしまう。確かに overlap がない場合の処理ができないことに気づく +* ツギハギのようで嫌だなあと思いながら、`SolutionWA2` のようにフラグを立ててみる + * 最初、inner while にだけフラグを立てて、IN: `[[1,3],[6,9]], [2, 5]` -> OUT: `[[1,5],[6,9],[2,5]]` のようになってしまった + * `if (new_interval_start <= interval_end)` の中でもフラグを立てるようにした + * IN `[[1,5]], [0, 3]` -> OUT `[[1,5]]` (expect `[0, 5]`) のように前が extend されるケースが考慮されていない。 + * 1h 以上悩んだので一旦力尽きた + +### step2 + +* https://github.com/huyfififi/coding-challenges/pull/26/files + * 一旦 step2 を見る。なるほど、overlap が発生する前、発生して skip する処理、残りの処理と段階を踏めばよかったのか… + * アイデアを見た段階で書き直した +* `step2.Solution`: ようやく AC した… + * これ、ループの条件で少し悩んだ。とぃうのも、1 つめのループと 2 つめのループで、メインの処理対象が異なる感覚。 + interval insertion みたいなのを考えるとき、2 本の数直線的なものを脳内でイメージしているのだが + * Loop1: `intervals[i]` が先行する数直線で、`newInterval` が後続。したがって、`intervals[i]` の end と、`newInterval` の start を比較。ここでは `insertion_start` と `insertion_end` は更新されない。 + * Loop2: `newInterval` (というか厳密には `insertion_start`, `insertion_end` で定義される区間 )が先行する数直線で、`intervals[i]` が後続。したがって、`newInterval` (というか `[insertion_start, insertion_en]`) の end と、`intervals[i]` の start を比較。 + * ちなみに step1 の extend していく感覚だと、なんとなく後ろにのみ extend される感覚があり、start の方の更新を忘れそう (step1 では実際忘れた) + * のように整理していないと、何と何を比較すればよいのかわからなくなりそう。 + + * 余力がなかったので再帰についての部分はスキップした +* https://github.com/huyfififi/coding-challenges/pull/26/files#r2184884440 + * `newInterval` を `intervals` に放り込んでソートしてから処理する、という発想はなかった (以下一応転載) + +```cpp +class Solution { +public: + vector> insert(vector>& intervals, vector& newInterval) { + intervals.push_back(newInterval); + std::sort(intervals.begin(), intervals.end()); + vector> merged; + vector last_interval = intervals.front(); + for (const auto& interval : intervals) { + if (last_interval[1] < interval[0]) { + merged.push_back(last_interval); + last_interval = interval; + } else { + last_interval[1] = max(last_interval[1], interval[1]); + } + } + + merged.push_back(last_interval); + return merged; + } +}; +``` + +* Python の `extend` + slice を C++ で書くとこんなかんじらしい。最後の while を書き換えられる + * とはいえ C++ だと while を使って書きたくなってしまう。十分単純なので。Python だと extend + slice という特有の syntax / function があるからいい気がするけど… + + +```py +result.extend(intervals[i:]) +``` + +```cpp +result.insert(result.end(), intervals.begin() + i, intervals.end()); +``` + +* しかし今回のドツボから学べることはなんだろう?またやりそうで怖い。 + * そもそも最初の、全部 1 ループで処理しようというのが複雑にする要因だったのだが、まあそれが必要なとき (inner loop) もあるわけで、どうやって見切りをつけるべきだったのか… + * なんか時々こういうのある気はする。ある特定の方針で書くと edge ケースの対応が妙にややこしくなるんだけど、別の procedure で考えると簡単、みたいな。ハマったときに切り替えられるとよいのだが面接でできる気がしないかも + +### step3 + +* skip diff --git a/57_insert_interval/step1.cpp b/57_insert_interval/step1.cpp new file mode 100644 index 0000000..bd73b47 --- /dev/null +++ b/57_insert_interval/step1.cpp @@ -0,0 +1,61 @@ +class SolutionWA1 { +public: + vector> insert(vector>& intervals, vector& newInterval) { + int new_interval_start = newInterval[0]; + int new_interval_end = newInterval[1]; + std::vector> result; + int i = 0; + bool has_overlap = false; + while (i < intervals.size()) { + int interval_start = intervals[i][0]; // Fixed + int interval_end = intervals[i][1]; // Temp. end. Not fixed yet. + if (new_interval_start <= interval_end) { + interval_end = std::max(interval_end, new_interval_end); + } + int j = i + 1; + + while (j < intervals.size() && intervals[j][0] <= interval_end) { + interval_end = std::max(interval_end, intervals[j][1]); + ++j; + } + result.push_back({interval_start, interval_end}); + i = j; + } + return result; + } +}; + + + +class SolutionWA2 { +public: + vector> insert(vector>& intervals, vector& newInterval) { + int new_interval_start = newInterval[0]; + int new_interval_end = newInterval[1]; + std::vector> result; + int i = 0; + bool has_overlap = false; + while (i < intervals.size()) { + int interval_start = intervals[i][0]; // Fixed + int interval_end = intervals[i][1]; // Temp. end. Not fixed yet. + if (new_interval_start <= interval_end) { + has_overlap = true; + interval_end = std::max(interval_end, new_interval_end); + } + int j = i + 1; + + while (j < intervals.size() && intervals[j][0] <= interval_end) { + has_overlap = true; + interval_end = std::max(interval_end, intervals[j][1]); + ++j; + } + result.push_back({interval_start, interval_end}); + i = j; + } + if (!has_overlap) { + result.push_back(newInterval); + return result; + } + return result; + } +}; diff --git a/57_insert_interval/step2.cpp b/57_insert_interval/step2.cpp new file mode 100644 index 0000000..bc4e4d7 --- /dev/null +++ b/57_insert_interval/step2.cpp @@ -0,0 +1,26 @@ +class Solution { +public: + vector> insert(vector>& intervals, vector& newInterval) { + std::vector> result; + int insertion_start = newInterval[0]; + int insertion_end = newInterval[1]; + int i = 0; + // No overlap -> push_back as is. + while (i < intervals.size() && intervals[i][1] < insertion_start) { + result.push_back(intervals[i++]); + } + // Handle overlaps + while (i < intervals.size() && intervals[i][0] <= insertion_end) { + insertion_start = std::min(insertion_start, intervals[i][0]); + insertion_end = std::max(insertion_end, intervals[i][1]); + ++i; + } + result.push_back({insertion_start, insertion_end}); + + // The rest (if remaining) + while (i < intervals.size()) { + result.push_back(intervals[i++]); + } + return result; + } +}; diff --git a/57_insert_interval/step3.cpp b/57_insert_interval/step3.cpp new file mode 100644 index 0000000..e69de29 diff --git a/57_insert_interval/step4.cpp b/57_insert_interval/step4.cpp new file mode 100644 index 0000000..332855c --- /dev/null +++ b/57_insert_interval/step4.cpp @@ -0,0 +1,135 @@ +// Response to +// https://github.com/ryosuketc/leetcode_grind75/pull/26/files#r2543575724 + +// 最初こんな感じで書いてみようとしてだめでした。手でやるのをイメージして書いていたんですが、それだと結局 merge が完了しているのかどうかのフラグを脳内に持っている感じがあり、それが必要なのかなという気がします。ただいずれにせよ条件判定が複雑になるような気がしました。 + +class Solution { +public: + vector> insert(vector>& intervals, vector& newInterval) { + std::vector> merged_intervals; + for (auto& interval : intervals) { + if (!Overlap(interval, newInterval)) { + merged_intervals.push_back(interval); + continue; + } + // where to insert newInterval? + // merge_done みたいなフラグをもつ必要がある気がする (手でやるならそうしそうな気がする)。 + // back returns a reference. + std::vector& last_interval = merged_intervals.back(); + last_interval = Merge(last_interval, interval); + } + return merged_intervals; + } +private: + bool Overlap(const std::vector& interval1, const std::vector& interval2) { + // Get overlapped interval (not merged, but overlaped part). + int overlap_start = std::max(interval1[0], interval2[0]); + int overlap_end = std::min(interval1[1], interval2[1]); + return overlap_start <= overlap_end; + // 2 interval を始点でソートして比べたほうがわかりやすい気はする + } + std::vector Merge(const std::vector& interval1, const std::vector& interval2) { + int merged_start = std::min(interval1[0], interval2[0]); + int merged_end = std::max(interval1[1], interval2[1]); + return {merged_start, merged_end}; + } +}; + +// フラグがややこしいのかなと思って、1 回だけ入るループのような想定で書いてみました。こんなイメージ。 + +// 1. newInterval が書いたカードが手持ち。interval が書いたカードを順番に取っていく (sort されている) +// 2. 手持ちの newInterval と被っていなければ順次 interval を追加していく +// 3. 被ったらまずそこで取った interval と newInterval を merge -> 被りがなくなるまで次の interval を取り続ける。この時点で newInterval を書いたカードは手持ちからなくなっているので merge は終わり。 +// 4. 被りがなくなったら残りを追加していく + +// 116 / 158 testcases passed ではありますが、`[]` に `[5,7]` を挿入するなどのケースで落ちますね。 +// もし newInterval がどの区間とも重ならず、かつ既存の区間の間や先頭にある場合、newInterval はどこにも追加されずに処理が終わってしまうんですね。現在のロジックでは、!Overlap(重なりなし)の場合、intervals[i] を push_back するだけなので。 + +class Solution { +public: + vector> insert(vector>& intervals, vector& newInterval) { + std::vector> merged_intervals; + int i = 0; + while (i < intervals.size()) { + if (!Overlap(intervals[i], newInterval)) { + merged_intervals.push_back(intervals[i]); + ++i; + continue; + } + if (Overlap(intervals[i], newInterval)) { + std::vector merged_interval = Merge(intervals[i], newInterval); + ++i; + while (i < intervals.size() && Overlap(intervals[i], merged_interval)) { + merged_interval = Merge(intervals[i], merged_interval); + ++i; + } + merged_intervals.push_back(merged_interval); + } + } + return merged_intervals; + } +private: + bool Overlap(const std::vector& interval1, const std::vector& interval2) { + // Get overlapped interval (not merged, but overlaped part). + int overlap_start = std::max(interval1[0], interval2[0]); + int overlap_end = std::min(interval1[1], interval2[1]); + return overlap_start <= overlap_end; + // 2 interval を始点でソートして比べたほうがわかりやすい気はする + } + std::vector Merge(const std::vector& interval1, const std::vector& interval2) { + int merged_start = std::min(interval1[0], interval2[0]); + int merged_end = std::max(interval1[1], interval2[1]); + return {merged_start, merged_end}; + } +}; + +// 最終的に Gemini に直してもらったらこうなりましたが、結局 3 パターンに分類しており、3 つループを書いたほうがわかりやすいですね。 + +class Solution { +public: + vector> insert(vector>& intervals, vector& newInterval) { + std::vector> merged_intervals; + int i = 0; + bool inserted = false; // newInterval を挿入したかどうかのフラグ + + while (i < intervals.size()) { + // ケース1: intervals[i] が newInterval より完全に「右」にある + // -> 先に newInterval を入れる必要がある + if (intervals[i][0] > newInterval[1]) { + if (!inserted) { + merged_intervals.push_back(newInterval); + inserted = true; + } + merged_intervals.push_back(intervals[i]); + i++; + } + // ケース2: intervals[i] が newInterval より完全に「左」にある + // -> intervals[i] をそのまま入れる + else if (intervals[i][1] < newInterval[0]) { + merged_intervals.push_back(intervals[i]); + i++; + } + // ケース3: 重なっている (Overlap) + // -> マージして newInterval を更新し続ける(まだ push しない) + else { + newInterval = Merge(intervals[i], newInterval); + i++; + } + } + + // ループ終了後、まだ newInterval が入っていなければ最後に追加 + if (!inserted) { + merged_intervals.push_back(newInterval); + } + + return merged_intervals; + } + +private: + // Merge関数はそのまま利用可能 + std::vector Merge(const std::vector& interval1, const std::vector& interval2) { + int merged_start = std::min(interval1[0], interval2[0]); + int merged_end = std::max(interval1[1], interval2[1]); + return {merged_start, merged_end}; + } +};