Skip to content

57. Insert Interval#26

Open
ryosuketc wants to merge 5 commits intomainfrom
57_insert_interval
Open

57. Insert Interval#26
ryosuketc wants to merge 5 commits intomainfrom
57_insert_interval

Conversation

@ryosuketc
Copy link
Owner

* 最初、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 以上悩んだので一旦力尽きた
Copy link

Choose a reason for hiding this comment

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

えー、なんか素直ではない気がしています。
日本語で説明したら、「インターバルが並んでて、新しいインターバルと被ってるやつをすべてくっつける。」のですよね。
まず、「2つのインターバルを引数に取って被ってるかを判定する関数」作りませんか。で、次に「2つのインターバルをくっつける関数」です。この2つあればできませんか。

Copy link
Owner Author

Choose a reason for hiding this comment

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

step4.cpp に同じ内容をあげています。結局うまくいきませんでした…


最初こんな感じで書いてみようとしてだめでした。手でやるのをイメージして書いていたんですが、それだと結局 merge が完了しているのかどうかのフラグを脳内に持っている感じがあり、それが必要なのかなという気がします。ただいずれにせよ条件判定が複雑になるような気がしました。

class Solution {
public:
    vector<vector<int>> insert(vector<vector<int>>& intervals, vector<int>& newInterval) {
        std::vector<std::vector<int>> 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<int>& last_interval = merged_intervals.back();
            last_interval = Merge(last_interval, interval);
        }
        return merged_intervals;
    }
private:
    bool Overlap(const std::vector<int>& interval1, const std::vector<int>& 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<int> Merge(const std::vector<int>& interval1, const std::vector<int>& 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<vector<int>> insert(vector<vector<int>>& intervals, vector<int>& newInterval) {
        std::vector<std::vector<int>> 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<int> 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<int>& interval1, const std::vector<int>& 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<int> Merge(const std::vector<int>& interval1, const std::vector<int>& 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<vector<int>> insert(vector<vector<int>>& intervals, vector<int>& newInterval) {
        std::vector<std::vector<int>> 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<int> Merge(const std::vector<int>& interval1, const std::vector<int>& interval2) {
        int merged_start = std::min(interval1[0], interval2[0]);
        int merged_end = std::max(interval1[1], interval2[1]);
        return {merged_start, merged_end};
    }
};

Copy link

Choose a reason for hiding this comment

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

あ、いえ、ループ3つで最終的にはいいんですが、発想として何がひっかかっているかについて考えたいというものです。

  • 先頭から intervals を見ていく。左にあるやつはそのまま出力。
  • オーバーラップしていたら、newInterval を更新。(破壊するのが嫌ならば別の変数にあらかじめコピーしておく。)
  • オーバーラップしなくなったら newInterval を出力。
  • 残りをすべて出力。

これが私は一番素直だと思いました。Gemini に書かせただけのコードだとこんなイメージです。こっちを頭に浮かべて、queue にする必要がないなど簡略化すると、Step 2 になるでしょう。
私がひっかかっていると予想しているのは、左にあるか、オーバーラップしているかどうか、という高次の概念と、[0][1]の比較という低次の操作の距離が遠いので、やっているうちに混乱するというものです。つまり、人間、頭の中に持っておけるものは多くないので、低次の操作と高次の概念の対応関係を一回手放したいということです。そのために一旦関数にして考えてはどうかというアドバイスです。あと、範囲 for 文で回そうとしたのも理由でしょうか。

class Solution {
public:
    std::vector<std::vector<int>> insert(std::vector<std::vector<int>>& intervals, std::vector<int>& newInterval) {
        
        // 1. データ準備
        // intervals を queue にコピー (先頭から順に取り出すため)
        std::queue<std::vector<int>> intervals_queue;
        for (const auto& interval : intervals) {
            intervals_queue.push(interval);
        }
        
        // newInterval を作業用変数にコピー(オリジナルの newInterval を破壊しないため)
        std::vector<int> current_merged_interval = newInterval;
        
        // 出力用の結果リスト
        std::vector<std::vector<int>> result;

        // --- フェーズ 1: 左側の非オーバーラップ部分を処理 (左にあるやつはそのまま出力) ---
        // queue の先頭が current_merged_interval より完全に左にある間、結果に追加
        while (!intervals_queue.empty() && intervals_queue.front()[1] < current_merged_interval[0]) {
            result.push_back(intervals_queue.front());
            intervals_queue.pop();
        }

        // --- フェーズ 2: オーバーラップ部分を処理 (オーバーラップしていたら current_merged_interval を更新) ---
        // queue の先頭が current_merged_interval とオーバーラップしている間、マージして current_merged_interval を更新
        // オーバーラップの条件: 区間Aの始まり <= 区間Bの終わり AND 区間Bの始まり <= 区間Aの終わり
        while (!intervals_queue.empty() && Overlap(intervals_queue.front(), current_merged_interval)) {
            // マージして current_merged_interval を拡張
            current_merged_interval = Merge(intervals_queue.front(), current_merged_interval);
            intervals_queue.pop();
        }

        // --- フェーズ 3: マージ結果の挿入 (オーバーラップしなくなったら current_merged_interval を出力) ---
        result.push_back(current_merged_interval);

        // --- フェーズ 4: 右側の非オーバーラップ部分を処理 (残りをすべて出力) ---
        // queue に残っているインターバル(すべて current_merged_interval より右にある)を結果に追加
        while (!intervals_queue.empty()) {
            result.push_back(intervals_queue.front());
            intervals_queue.pop();
        }

        return result;
    }

private:
    // 2つのインターバルが重なっているかチェック
    bool Overlap(const std::vector<int>& interval1, const std::vector<int>& interval2) {
        // A.start <= B.end && B.start <= A.end
        return interval1[0] <= interval2[1] && interval2[0] <= interval1[1];
    }
    
    // 2つのインターバルをマージ
    std::vector<int> Merge(const std::vector<int>& interval1, const std::vector<int>& interval2) {
        int merged_start = std::min(interval1[0], interval2[0]);
        int merged_end = std::max(interval1[1], interval2[1]);
        return {merged_start, merged_end};
    }
};

Copy link
Owner Author

Choose a reason for hiding this comment

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

なるほど、意図は理解しました。
また範囲 for / for を使おうとしたのも確かに一因であるように思います。

この問題、そんなに難しくはない気がするのですが、私がやった方針で行くとかなりハマってしまって (計算量ではなく、処理やフラグ管理が複雑化する)、一方で3ループに分ける方針だと素直に実装できる印象でした。簡単そうな問題であるんだけどコードに落とそうとすると煩雑そうで手が止まる感じがありました。

こうした形のハマり方をときどきする印象があり、原因や対処などありますかね…

Copy link

Choose a reason for hiding this comment

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

なるほど。発想のときに「手でできるか」の後に「複数人でシフトを組んでできるか」という話をしています。

なぜ、上で考えるときに queue に移したか、というとテッシュのボックスみたいに、一枚ずつ紙を引っ張ると出てくるような仕組みのほうが、複数人でシフトを組む上で都合がいいからです。できることが制限されていますからランダムアクセスできる vector と一つずつ増やすインデックスの組より都合がいいです。でも、最終的には後者にするでしょう。

フラグの管理をするというのは、複数人でシフトを組む上では、部屋に大きなホワイトボードを置いておいて、従業員たちに参照や書き換えをさせるということです。一方行にしか状態が遷移していかないならば、別のマニュアルにしてマニュアルの取り換えをしたほうがいいでしょう。

意味として同じもので表現として違うものがあるときにどちらを取るか変形するか、みたいな高次の力が弱そうです。

Copy link

Choose a reason for hiding this comment

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

ここのコードの整え方みたいな話です。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.9kpbwslvv3yv

また、逆方向の抽象化もあるかもしれません。intervals1 と intervals2 という2つのティッシュボックスがあって、全部のテッシュをマージしたいです。
intervals2 には newInterval と書かれたティッシュだけ入れればいいです。

Copy link

Choose a reason for hiding this comment

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

また、Gemini

class Solution {
public:
    // メインの汎用マージ関数
    std::vector<std::vector<int>> mergeIntervals(
        const std::vector<std::vector<int>>& intervals1,
        const std::vector<std::vector<int>>& intervals2) 
    {
        std::vector<std::vector<int>> result;
        int i = 0; // intervals1 のポインター
        int j = 0; // intervals2 のポインター

        // 1. ポインター i と j を使って、intervals1 と intervals2 をマージし、オーバーラップを処理
        while (i < intervals1.size() || j < intervals2.size()) {
            
            std::vector<int> current_interval;

            // どちらのリストから次のインターバルを取り出すか決定
            if (i < intervals1.size() && (j >= intervals2.size() || intervals1[i][0] <= intervals2[j][0])) {
                // intervals1[i] の方が先か、intervals2 がもう空の場合
                current_interval = intervals1[i++];
            } else {
                // intervals2[j] の方が先
                current_interval = intervals2[j++];
            }

            // result が空、または current_interval が result の最後の要素とオーバーラップしない場合
            if (result.empty() || current_interval[0] > result.back()[1]) {
                // 新しいインターバルとして追加
                result.push_back(current_interval);
            } else {
                // オーバーラップしている場合、result の最後の要素とマージして更新
                result.back()[1] = std::max(result.back()[1], current_interval[1]);
            }
        }

        return result;
    }

    // 元の問題 (特殊ケース) を汎用関数で実装
    std::vector<std::vector<int>> insert(
        std::vector<std::vector<int>>& intervals, 
        std::vector<int>& newInterval) 
    {
        // newInterval を vector<vector<int>> の形式に変換
        std::vector<std::vector<int>> intervals2 = {newInterval};
        
        // 汎用マージ関数を呼び出す
        return mergeIntervals(intervals, intervals2);
    }
};

Copy link

Choose a reason for hiding this comment

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

こういうのもできますね。

class Solution {
private:
    using IntervalPointer = std::tuple<int, int, int>; // {start, list_idx, interval_idx}

public:
    std::vector<std::vector<int>> mergeMultipleIntervalLists(
        const std::vector<std::vector<std::vector<int>>>& multiple_interval_lists) 
    {
        std::priority_queue<
            IntervalPointer, 
            std::vector<IntervalPointer>, 
            std::greater<IntervalPointer>
        > pq;

        for (int i = 0; i < multiple_interval_lists.size(); ++i) {
            if (!multiple_interval_lists[i].empty()) {
                const auto& interval = multiple_interval_lists[i][0];
                pq.push({interval[0], i, 0});
            }
        }

        std::vector<std::vector<int>> result;

        while (!pq.empty()) {
            auto [start, list_idx, interval_idx] = pq.top();
            pq.pop();
            const auto& current_interval = multiple_interval_lists[list_idx][interval_idx];
            int next_interval_idx = interval_idx + 1;
            if (next_interval_idx < multiple_interval_lists[list_idx].size()) {
                const auto& next_interval = multiple_interval_lists[list_idx][next_interval_idx];
                pq.push({next_interval[0], list_idx, next_interval_idx});
            }

            if (result.empty() || current_interval[0] > result.back()[1]) {
                result.push_back(current_interval);
            } else {
                result.back()[1] = std::max(result.back()[1], current_interval[1]);
            }
        }
        return result;
    }
};

class Solution {
public:
vector<vector<int>> insert(vector<vector<int>>& intervals, vector<int>& newInterval) {
intervals.push_back(newInterval);
Copy link

Choose a reason for hiding this comment

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

入力を変更している点が気になりました。呼び出し側の視点に立って考えると、関数に渡した値が勝手に書き換えられているとびっくりすると思います。入力は原則変更しないか、変更する場合は関数コメントでそれを明記することをおすすめします。

@ryosuketc
Copy link
Owner Author

ryosuketc commented Nov 21, 2025 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants