Conversation
| * 最初、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 以上悩んだので一旦力尽きた |
There was a problem hiding this comment.
えー、なんか素直ではない気がしています。
日本語で説明したら、「インターバルが並んでて、新しいインターバルと被ってるやつをすべてくっつける。」のですよね。
まず、「2つのインターバルを引数に取って被ってるかを判定する関数」作りませんか。で、次に「2つのインターバルをくっつける関数」です。この2つあればできませんか。
There was a problem hiding this comment.
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 回だけ入るループのような想定で書いてみました。こんなイメージ。
- newInterval が書いたカードが手持ち。interval が書いたカードを順番に取っていく (sort されている)
- 手持ちの newInterval と被っていなければ順次 interval を追加していく
- 被ったらまずそこで取った interval と newInterval を merge -> 被りがなくなるまで次の interval を取り続ける。この時点で newInterval を書いたカードは手持ちからなくなっているので merge は終わり。
- 被りがなくなったら残りを追加していく
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};
}
};There was a problem hiding this comment.
あ、いえ、ループ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};
}
};There was a problem hiding this comment.
なるほど、意図は理解しました。
また範囲 for / for を使おうとしたのも確かに一因であるように思います。
この問題、そんなに難しくはない気がするのですが、私がやった方針で行くとかなりハマってしまって (計算量ではなく、処理やフラグ管理が複雑化する)、一方で3ループに分ける方針だと素直に実装できる印象でした。簡単そうな問題であるんだけどコードに落とそうとすると煩雑そうで手が止まる感じがありました。
こうした形のハマり方をときどきする印象があり、原因や対処などありますかね…
There was a problem hiding this comment.
なるほど。発想のときに「手でできるか」の後に「複数人でシフトを組んでできるか」という話をしています。
なぜ、上で考えるときに queue に移したか、というとテッシュのボックスみたいに、一枚ずつ紙を引っ張ると出てくるような仕組みのほうが、複数人でシフトを組む上で都合がいいからです。できることが制限されていますからランダムアクセスできる vector と一つずつ増やすインデックスの組より都合がいいです。でも、最終的には後者にするでしょう。
フラグの管理をするというのは、複数人でシフトを組む上では、部屋に大きなホワイトボードを置いておいて、従業員たちに参照や書き換えをさせるということです。一方行にしか状態が遷移していかないならば、別のマニュアルにしてマニュアルの取り換えをしたほうがいいでしょう。
意味として同じもので表現として違うものがあるときにどちらを取るか変形するか、みたいな高次の力が弱そうです。
There was a problem hiding this comment.
ここのコードの整え方みたいな話です。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.9kpbwslvv3yv
また、逆方向の抽象化もあるかもしれません。intervals1 と intervals2 という2つのティッシュボックスがあって、全部のテッシュをマージしたいです。
intervals2 には newInterval と書かれたティッシュだけ入れればいいです。
There was a problem hiding this comment.
また、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);
}
};There was a problem hiding this comment.
こういうのもできますね。
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); |
There was a problem hiding this comment.
入力を変更している点が気になりました。呼び出し側の視点に立って考えると、関数に渡した値が勝手に書き換えられているとびっくりすると思います。入力は原則変更しないか、変更する場合は関数コメントでそれを明記することをおすすめします。
|
なるほど、、手でどうするかというのは考える癖がついているのですが、シフトの部分はよく言われているもののあまり意識していなかったように思います。今後考えてみます。
意味として同じもので表現として違うものがあるときにどちらを取るか変形するか、みたいな高次の力が弱そうです。
うーん、、このあたりはどう意識・練習すればよいのか、あるいは実際ハマったときにどう対応するか考えかねている部分はあるのですが、ひとまずはハマりそうだったら
step back して別の実装、アルゴリズムが全く異なるというよりは、おっしゃるような、意味は同じで表現を変えられないか考えてみます。。
…On Fri, Nov 21, 2025, 13:32 oda ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In 57_insert_interval/memo.md
<#26 (comment)>
:
> +
+## 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 以上悩んだので一旦力尽きた
なるほど。発想のときに「手でできるか」の後に「複数人でシフトを組んでできるか」という話をしています。
なぜ、上で考えるときに queue
に移したか、というとテッシュのボックスみたいに、一枚ずつ紙を引っ張ると出てくるような仕組みのほうが、複数人でシフトを組む上で都合がいいからです。できることが制限されていますからランダムアクセスできる
vector と一つずつ増やすインデックスの組より都合がいいです。でも、最終的には後者にするでしょう。
フラグの管理をするというのは、複数人でシフトを組む上では、部屋に大きなホワイトボードを置いておいて、従業員たちに参照や書き換えをさせるということです。一方行にしか状態が遷移していかないならば、別のマニュアルにしてマニュアルの取り換えをしたほうがいいでしょう。
意味として同じもので表現として違うものがあるときにどちらを取るか変形するか、みたいな高次の力が弱そうです。
—
Reply to this email directly, view it on GitHub
<#26 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AKJ2DZQFAIGEBQSWLLZ4KZT352IW7AVCNFSM6AAAAACMTOYVFGVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZTIOJRGE2TAMRRGU>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
57. Insert Interval
https://leetcode.com/problems/insert-interval/